<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kto on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/kto/</link><description>Recent content in Kto on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 21 May 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/kto/index.xml" rel="self" type="application/rss+xml"/><item><title>Fine-tuning continuo en producción: del tráfico real al adapter desplegado</title><link>https://blog.lo0.es/posts/fine-tuning-continuo-produccion/</link><pubDate>Thu, 21 May 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/fine-tuning-continuo-produccion/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Fine-tuning continuo no es &amp;ldquo;entrenar el modelo cada cierto tiempo&amp;rdquo;. Es un &lt;strong>ciclo cerrado&lt;/strong> donde el tráfico real de producción genera los datasets, un pipeline corto entrena un adapter LoRA, una batería de evaluaciones decide si promociona, y vLLM lo carga &lt;strong>sin reiniciar&lt;/strong>. El estado del arte en mayo de 2026 ha fragmentado el stack: ya no es DPO contra todo, sino una elección entre SFT, DPO, KTO, ORPO y SimPO según el tipo de señal que captura tu producto. Lo que ha consolidado el patrón es la combinación PostgreSQL 18 + pgvector 0.8 como &lt;strong>sistema nervioso del pipeline&lt;/strong> —captura de tráfico, dataset versioning, eval results, registry de adapters—, junto a vLLM multi-LoRA hot-swap que convierte el despliegue en una llamada HTTP. Este artículo desmonta el ciclo con esquemas concretos, queries reales, y los números que cuestan en una RTX 4090 frente a un cluster 4×H100.&lt;/p>
&lt;h2 id="la-analogía-el-restaurante-que-afina-su-carta">La analogía: el restaurante que afina su carta&lt;/h2>
&lt;p>Imagina un restaurante de barrio con un plato estrella que funciona, pero el chef sabe que se puede afinar. Cada noche pasan cosas:&lt;/p>
&lt;ul>
&lt;li>Algunos comensales &lt;strong>dejan parte del plato&lt;/strong>: señal débil de que algo no acabó de encajar.&lt;/li>
&lt;li>Otros piden &lt;strong>otra versión&lt;/strong> (&amp;quot;¿podrías ponerle menos sal?&amp;quot;): señal explícita y direccional.&lt;/li>
&lt;li>Otros &lt;strong>terminan el plato y vuelven la semana siguiente&lt;/strong>: la única señal que de verdad importa, pero llega tarde.&lt;/li>
&lt;li>Y un grupo selecto opina sin que se les pregunte, normalmente para mal.&lt;/li>
&lt;/ul>
&lt;p>El chef no rehace su carta cada noche. Hace algo más interesante: anota en una libreta los platos servidos, las devoluciones, los cambios pedidos, las propinas. Cada cierto tiempo, &lt;strong>lee la libreta entera&lt;/strong>, decide ajustes mínimos en una receta, prueba la nueva versión en mesa privada con su personal, y solo si la prueban favorablemente la incorpora a la carta del día siguiente. A veces incluso sirve dos versiones distintas del plato a distintas mesas durante una semana, mide qué pasa, y elige.&lt;/p>
&lt;p>Eso es &lt;strong>fine-tuning continuo&lt;/strong>. La libreta es Postgres. El plato es el modelo base. Las anotaciones son señales de feedback —explícitas y implícitas—. El &amp;ldquo;ajuste mínimo&amp;rdquo; es un LoRA adapter de 30 MB. La mesa privada es la batería de evaluaciones automatizadas. La carta del día siguiente es vLLM con multi-LoRA hot-swap, que carga el nuevo adapter sin reiniciar el servicio. El servir dos versiones a distintas mesas es A/B testing con tráfico real.&lt;/p>
&lt;p>La analogía es exacta en un punto crítico: &lt;strong>el chef no tira la receta original&lt;/strong>. Mantiene la receta base y guarda una libreta separada con las &amp;ldquo;modificaciones que dan buen resultado para los habituales del barrio&amp;rdquo;. Esa libreta es el adapter LoRA: encima del modelo base, no en su lugar.&lt;/p>
&lt;h2 id="el-ciclo-desmontado">El ciclo, desmontado&lt;/h2>
&lt;p>Antes de entrar en componentes, conviene fijar el flujo completo. Estos siete pasos son lo que cualquier equipo serio replica con variaciones:&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Ciclo cerrado de fine-tuning continuo">
&lt;style>
.node { fill: #f4f4f4; stroke: #333; stroke-width: 1.5; }
.n-serve { fill: #d6eaff; }
.n-data { fill: #ffe9d6; }
.n-train { fill: #d9f5d6; }
.n-eval { fill: #f4e1ff; }
.lbl { font: 600 12px sans-serif; fill: #222; }
.lbl-sm { font: 10.5px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.4; fill: none; marker-end: url(#ah2); }
.arr-dim { stroke: #888; stroke-width: 1.2; fill: none; stroke-dasharray: 4,3; marker-end: url(#ah2); }
&lt;/style>
&lt;defs>
&lt;marker id="ah2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>
&lt;/marker>
&lt;/defs>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">El ciclo cerrado de fine-tuning continuo&lt;/text>
&lt;rect x="280" y="40" width="160" height="50" rx="8" class="node n-serve"/>
&lt;text x="360" y="62" text-anchor="middle" class="lbl">1 · vLLM serving&lt;/text>
&lt;text x="360" y="78" text-anchor="middle" class="lbl-sm">base + adapters activos&lt;/text>
&lt;rect x="500" y="120" width="170" height="50" rx="8" class="node n-data"/>
&lt;text x="585" y="142" text-anchor="middle" class="lbl">2 · Captura tráfico&lt;/text>
&lt;text x="585" y="158" text-anchor="middle" class="lbl-sm">prompts, respuestas, feedback&lt;/text>
&lt;rect x="500" y="210" width="170" height="50" rx="8" class="node n-data"/>
&lt;text x="585" y="232" text-anchor="middle" class="lbl">3 · Curación&lt;/text>
&lt;text x="585" y="248" text-anchor="middle" class="lbl-sm">dedup, PII, balanceo, snapshot&lt;/text>
&lt;rect x="280" y="290" width="160" height="50" rx="8" class="node n-train"/>
&lt;text x="360" y="312" text-anchor="middle" class="lbl">4 · Training LoRA&lt;/text>
&lt;text x="360" y="328" text-anchor="middle" class="lbl-sm">SFT / DPO / KTO / ORPO / SimPO&lt;/text>
&lt;rect x="50" y="210" width="170" height="50" rx="8" class="node n-eval"/>
&lt;text x="135" y="232" text-anchor="middle" class="lbl">5 · Eval gates&lt;/text>
&lt;text x="135" y="248" text-anchor="middle" class="lbl-sm">3 etapas: PR, full, canary&lt;/text>
&lt;rect x="50" y="120" width="170" height="50" rx="8" class="node n-train"/>
&lt;text x="135" y="142" text-anchor="middle" class="lbl">6 · Adapter registry&lt;/text>
&lt;text x="135" y="158" text-anchor="middle" class="lbl-sm">status: canary | prod | retired&lt;/text>
&lt;rect x="50" y="40" width="170" height="50" rx="8" class="node n-serve"/>
&lt;text x="135" y="62" text-anchor="middle" class="lbl">7 · Hot-swap&lt;/text>
&lt;text x="135" y="78" text-anchor="middle" class="lbl-sm">POST /v1/load_lora_adapter&lt;/text>
&lt;path class="arr" d="M440,75 C480,80 495,100 510,120"/>
&lt;path class="arr" d="M585,170 L585,210"/>
&lt;path class="arr" d="M500,250 C460,270 440,280 440,300"/>
&lt;path class="arr" d="M280,315 C240,290 230,275 220,260"/>
&lt;path class="arr" d="M135,210 L135,170"/>
&lt;path class="arr" d="M135,120 L135,90"/>
&lt;path class="arr" d="M220,65 L280,65"/>
&lt;rect x="280" y="170" width="160" height="80" rx="10" fill="#fffae6" stroke="#d4a52a" stroke-width="2"/>
&lt;text x="360" y="200" text-anchor="middle" class="lbl">PostgreSQL 18&lt;/text>
&lt;text x="360" y="218" text-anchor="middle" class="lbl-sm">+ pgvector 0.8&lt;/text>
&lt;text x="360" y="234" text-anchor="middle" class="lbl-sm">single source of truth&lt;/text>
&lt;path class="arr-dim" d="M500,145 L440,180"/>
&lt;path class="arr-dim" d="M500,235 L440,225"/>
&lt;path class="arr-dim" d="M280,225 L220,235"/>
&lt;path class="arr-dim" d="M280,200 L220,160"/>
&lt;path class="arr-dim" d="M360,290 L360,250"/>
&lt;/svg>
&lt;/div>
&lt;p>El ciclo dura entre 1 y 4 semanas en producción real. Lo que cambia entre equipos es el ritmo (más rápido en chat asistente, más lento en banca regulada) y los detalles de cada paso. La estructura es la misma.&lt;/p>
&lt;h2 id="por-qué-fine-tuning-continuo-y-por-qué-no-es-rag">Por qué fine-tuning continuo (y por qué no es RAG)&lt;/h2>
&lt;p>Antes de profundizar, una distinción que se sigue confundiendo. &lt;strong>Fine-tuning sirve para forma, no para hechos.&lt;/strong> Si tu problema es que el modelo no conoce las tarifas del cliente o el catálogo actualizado, no fine-tunees: usa RAG. Si tu problema es que el modelo responde con un tono que no encaja, no respeta tu formato JSON, rechaza casos legítimos o se inventa estructura, ahí sí es fine-tuning.&lt;/p>
&lt;p>En 2026 el límite ya está bien establecido por la práctica de la comunidad:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Problema observado&lt;/th>
&lt;th>Solución&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>El modelo no sabe X (X cambia semanalmente)&lt;/td>
&lt;td>RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo conoce X pero responde mal de tono o formato&lt;/td>
&lt;td>Fine-tuning SFT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hay dos formas de responder y prefiero una sobre otra&lt;/td>
&lt;td>Fine-tuning con preferencias (DPO/KTO/ORPO/SimPO)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo razona mal en un dominio verificable (código, mates)&lt;/td>
&lt;td>RL con recompensa verificable (GRPO/DAPO)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo es competente, solo necesita memoria de hechos&lt;/td>
&lt;td>RAG, no fine-tuning&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Fine-tuning continuo es la versión disciplinada del segundo y tercer caso. La palabra clave es &lt;strong>continuo&lt;/strong>: no es un evento puntual de &amp;ldquo;alineamos el modelo&amp;rdquo;, es un proceso que toca cada vez que la distribución del tráfico se desvía lo suficiente, o que aparecen nuevos casos de uso.&lt;/p>
&lt;h2 id="las-cuatro-técnicas-según-la-señal-que-captures">Las cuatro técnicas según la señal que captures&lt;/h2>
&lt;p>El cambio más importante de los últimos 12 meses ha sido el fin del monopolio de DPO. En 2024 todo equipo que hacía alineamiento usaba DPO con pares &lt;code>(chosen, rejected)&lt;/code>. En 2026 la elección es más fina y depende de &lt;strong>cómo es la señal que recoges en tu producto&lt;/strong>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Señal real en producto&lt;/th>
&lt;th>Técnica recomendada&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Ejemplos correctos etiquetados (input → output esperado)&lt;/td>
&lt;td>&lt;strong>SFT + LoRA&lt;/strong>&lt;/td>
&lt;td>Sigue siendo la base. 500-5.000 ejemplos bastan para estilo.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pares explícitos &lt;code>(chosen, rejected)&lt;/code>&lt;/td>
&lt;td>&lt;strong>DPO&lt;/strong> o &lt;strong>SimPO&lt;/strong>&lt;/td>
&lt;td>SimPO elimina el modelo de referencia → 50 % menos VRAM en entrenamiento.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>👍 / 👎 sueltos sobre respuestas&lt;/td>
&lt;td>&lt;strong>KTO&lt;/strong>&lt;/td>
&lt;td>El método que más naturalmente encaja con la telemetría real.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SFT y preferencias en una sola pasada&lt;/td>
&lt;td>&lt;strong>ORPO&lt;/strong>&lt;/td>
&lt;td>Un solo modelo en memoria, evita el drift entre fases.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Recompensa verificable (tests, soluciones)&lt;/td>
&lt;td>&lt;strong>GRPO&lt;/strong> / &lt;strong>DAPO&lt;/strong>&lt;/td>
&lt;td>Razonamiento, no chat. Otro mundo.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La regla práctica: &lt;strong>diseña la captura de feedback en producto pensando en qué método podrás usar después&lt;/strong>. Si tu UI sólo tiene 👍/👎, fuerzas el camino a KTO. Si añades un botón &amp;ldquo;regenerar respuesta&amp;rdquo;, desbloqueas DPO desde el regenerate-as-rejected (lo veremos abajo). Si añades un botón &amp;ldquo;editar respuesta&amp;rdquo;, la respuesta editada se convierte en SFT directo de alta calidad.&lt;/p>
&lt;p>Hay un detalle de coste que se publicita poco. DPO necesita mantener en memoria &lt;strong>dos modelos&lt;/strong>: el que entrenas y el de referencia. SimPO elimina ese segundo modelo. ORPO también. Para un Llama 3 8B en BF16 esto es la diferencia entre necesitar ~32 GB de VRAM activos durante entrenamiento (DPO) o ~16 GB (SimPO/ORPO). Es la diferencia entre que el entrenamiento quepa en una RTX 4090 con QLoRA agresivo, o no quepa sin offload.&lt;/p>
&lt;h2 id="postgres-como-sistema-nervioso-del-pipeline">Postgres como sistema nervioso del pipeline&lt;/h2>
&lt;p>Aquí está la opinión técnica fuerte de este artículo, y es la que conviene defender con datos: &lt;strong>Postgres 18 + pgvector 0.8 + un bucket S3/MinIO para los pesos es suficiente para todo el pipeline&lt;/strong>. No hace falta MLflow, no hace falta lakeFS, no hace falta DVC.&lt;/p>
&lt;p>No se trata de minimalismo ideológico. Se trata de tres ventajas concretas que ningún stack alternativo iguala en el escenario on-premise con compliance:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Una sola fuente de verdad, un solo modelo de autorización.&lt;/strong> Las ACL que ya tienes para Postgres cubren los datos de entrenamiento, los resultados de eval, el registry de adapters y el log de auditoría. No multiplicas planos de control.&lt;/li>
&lt;li>&lt;strong>SQL como lenguaje universal del pipeline.&lt;/strong> El query que genera el dataset, el predicado del eval gate, la asignación de tráfico A/B, la decisión de promoción: todo es SQL. Tu equipo ya sabe SQL.&lt;/li>
&lt;li>&lt;strong>Audit y reproducibilidad criptográfica gratis.&lt;/strong> Las extensiones &lt;code>pg_audit&lt;/code> y &lt;code>pgcrypto&lt;/code>, combinadas con &lt;code>set_hash&lt;/code> sobre el dataset, te dan trazabilidad criptográfica sin código adicional. Es un terreno que da para artículo propio.&lt;/li>
&lt;/ol>
&lt;h3 id="esquema-concreto">Esquema concreto&lt;/h3>
&lt;p>Empezamos por la tabla de tráfico, particionada por semanas para que el &lt;code>DROP PARTITION&lt;/code> sea barato:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BIGSERIAL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">request_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">user_hash&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BYTEA&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- pseudonimización GDPR
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- ej. &amp;#34;support-es-v4.1&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">experiment&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- ej. &amp;#34;rerank-v2-canary&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">variant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">CHAR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;A&amp;#39; | &amp;#39;B&amp;#39; | NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">completion&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ttft_ms&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">tokens_in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">-- señales de feedback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SMALLINT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- -1/0/+1 (KTO-ready)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_regen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- usuario regeneró → DPO-rejected
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_edited&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- usuario editó → SFT golden
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">parent_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BIGINT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- autoreferencia regenerate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- vector y meta
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HALFVEC&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1024&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- pgvector 0.8, mitad de RAM
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pii_flags&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SMALLINT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- bitmask
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMPTZ&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RANGE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log_2026w21&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2026-05-18&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2026-05-25&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log_2026w21&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hnsw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">halfvec_cosine_ops&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log_2026w21&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tres decisiones merecen una nota:&lt;/p>
&lt;p>&lt;strong>&lt;code>HALFVEC(1024)&lt;/code>.&lt;/strong> Vectores en FP16 nativos de pgvector 0.8. La mitad de RAM y disco con pérdida de precisión irrelevante para deduplicación semántica. Esto solo, a escala de millones de filas, ahorra entre 4 y 8 GB.&lt;/p>
&lt;p>&lt;strong>Particionado semanal por rango temporal.&lt;/strong> A los 90 días, &lt;code>DROP TABLE obs.inference_log_2026wXX&lt;/code> libera espacio en milisegundos sin bloqueo prolongado. Autovacuum nunca vuelve a tocar particiones congeladas.&lt;/p>
&lt;p>&lt;strong>&lt;code>parent_id&lt;/code> autoreferenciado.&lt;/strong> El usuario regenera la respuesta → se inserta una nueva fila con &lt;code>parent_id&lt;/code> apuntando a la anterior. Eso nos dará un dataset DPO sin tocar la UX.&lt;/p>
&lt;h3 id="el-registry-de-adapters">El registry de adapters&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">base_model&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">rank&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">alpha&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">target_modules&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">method&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;sft&amp;#39;|&amp;#39;dpo&amp;#39;|&amp;#39;kto&amp;#39;|&amp;#39;orpo&amp;#39;|&amp;#39;simpo&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">training_run_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">dataset_snapshot_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">weights_uri&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- s3://.../v4.2.safetensors
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">eval_summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">status&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;training&amp;#39;|&amp;#39;canary&amp;#39;|&amp;#39;prod&amp;#39;|&amp;#39;retired&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">traffic_pct&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMERIC&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">promoted_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMPTZ&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El router de vLLM lee esta tabla con TTL de pocos segundos. Un &lt;code>UPDATE serve.adapter SET status='prod', traffic_pct=100 WHERE id='v4.2'&lt;/code> es una promoción. Un &lt;code>UPDATE ... SET status='retired'&lt;/code> es un rollback. La auditoría de quién hizo qué y cuándo la da &lt;code>pg_audit&lt;/code> sin escribir una línea de código adicional.&lt;/p>
&lt;h2 id="generar-datasets-dpo-y-kto-desde-tráfico-real">Generar datasets DPO y KTO desde tráfico real&lt;/h2>
&lt;p>Aquí es donde la elegancia del esquema paga. El dataset no es un fichero estático: es una &lt;strong>vista materializada&lt;/strong> que se construye con SQL sobre &lt;code>obs.inference_log&lt;/code>.&lt;/p>
&lt;h3 id="dataset-kto-desde-">Dataset KTO desde 👍/👎&lt;/h3>
&lt;p>KTO es el método que mejor encaja con la señal que captura cualquier producto de chat decente. La query:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MATERIALIZED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VIEW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kto_v3_candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">prompt&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">completion&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">CASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">THEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ELSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">false&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">label&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">interval&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;60 days&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pii_flags&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tenant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">consent_training&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Simple. Cada fila con feedback explícito se convierte en un ejemplo &lt;code>(prompt, response, deseable_sí_no)&lt;/code>. KTO entrena directamente sobre esta señal, sin necesidad de construir pares.&lt;/p>
&lt;h3 id="dataset-dpo-desde-regenerar">Dataset DPO desde &amp;ldquo;regenerar&amp;rdquo;&lt;/h3>
&lt;p>El truco que vale por sí solo este artículo. Cuando el usuario pulsa &amp;ldquo;regenerar respuesta&amp;rdquo;, está dando una señal extraordinariamente fuerte: la primera respuesta no le valió. Si la segunda no se regenera ni se valora negativamente, asumimos que sí. Eso es un par DPO sin un solo clic adicional en la UI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MATERIALIZED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VIEW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dpo_v3_candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">prompt&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">completion&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chosen&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">completion&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rejected&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">parent_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">fb_regen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">-- mitigación de length bias en DPO clásico
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BETWEEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La cláusula sobre longitudes es la cura barata al &lt;strong>length bias&lt;/strong> documentado en DPO. Sin ella, el modelo aprende que &amp;ldquo;más largo = mejor&amp;rdquo; porque las respuestas que el usuario acepta tienden a ser ligeramente más largas. Con SimPO o ORPO este filtro es opcional; con DPO clásico es necesario.&lt;/p>
&lt;h3 id="deduplicación-semántica-con-pgvector">Deduplicación semántica con pgvector&lt;/h3>
&lt;p>Antes de entrenar, dedup. Dos prompts casi idénticos en el dataset es ruido que sesga el modelo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ranked&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">row_number&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OVER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hashtext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">interval&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;60 days&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">DELETE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kto_v3_candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">kto&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ranked&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">rn&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">kto&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y para los duplicados semánticos (paráfrasis) usamos directamente pgvector 0.8 con &lt;code>iterative index scan&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- Buscar near-duplicates de un ejemplo cualquiera
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dist&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">interval&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;60 days&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">05&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El &lt;code>iterative scan&lt;/code> es una mejora clave de pgvector 0.8: antes, el índice HNSW podía devolver menos resultados de los pedidos cuando había filtros adicionales (&lt;code>WHERE&lt;/code>); ahora itera hasta cumplir el límite. Sin esa mejora, las queries de curación sobre datasets de millones de filas eran inviables sin un pre-filtro brutal.&lt;/p>
&lt;h2 id="eval-gates-tres-etapas-todo-sql">Eval gates: tres etapas, todo SQL&lt;/h2>
&lt;p>El error más común al implementar fine-tuning continuo es saltarse o aligerar los eval gates. Eso convierte el ciclo en una ruleta. El patrón que funciona en 2026 son &lt;strong>tres etapas&lt;/strong>, cada una con un trade-off latencia/cobertura distinto:&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Tres etapas de eval gates">
&lt;style>
.stage { stroke: #333; stroke-width: 1.5; }
.s1 { fill: #d6eaff; }
.s2 { fill: #d9f5d6; }
.s3 { fill: #ffe9d6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.6; fill: none; marker-end: url(#ah3); }
&lt;/style>
&lt;defs>
&lt;marker id="ah3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>
&lt;/marker>
&lt;/defs>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Tres etapas de eval gates&lt;/text>
&lt;rect x="30" y="50" width="200" height="110" rx="10" class="stage s1"/>
&lt;text x="130" y="78" text-anchor="middle" class="lbl">Stage 1 · PR&lt;/text>
&lt;text x="130" y="98" text-anchor="middle" class="lbl-sm">&amp;lt; 90 segundos&lt;/text>
&lt;text x="130" y="118" text-anchor="middle" class="lbl-sm">schema-lint + prompt-lint&lt;/text>
&lt;text x="130" y="135" text-anchor="middle" class="lbl-sm">+ 50 casos mini-eval&lt;/text>
&lt;rect x="260" y="50" width="200" height="110" rx="10" class="stage s2"/>
&lt;text x="360" y="78" text-anchor="middle" class="lbl">Stage 2 · pre-merge&lt;/text>
&lt;text x="360" y="98" text-anchor="middle" class="lbl-sm">&amp;lt; 20 minutos&lt;/text>
&lt;text x="360" y="118" text-anchor="middle" class="lbl-sm">200-500 casos golden&lt;/text>
&lt;text x="360" y="135" text-anchor="middle" class="lbl-sm">+ LLM-as-judge&lt;/text>
&lt;rect x="490" y="50" width="200" height="110" rx="10" class="stage s3"/>
&lt;text x="590" y="78" text-anchor="middle" class="lbl">Stage 3 · canary&lt;/text>
&lt;text x="590" y="98" text-anchor="middle" class="lbl-sm">24-72 horas&lt;/text>
&lt;text x="590" y="118" text-anchor="middle" class="lbl-sm">1-5 % tráfico real&lt;/text>
&lt;text x="590" y="135" text-anchor="middle" class="lbl-sm">métricas online + feedback&lt;/text>
&lt;path class="arr" d="M230,105 L260,105"/>
&lt;path class="arr" d="M460,105 L490,105"/>
&lt;/svg>
&lt;/div>
&lt;p>Y aquí es donde Postgres vuelve a brillar: el gate de promoción se expresa como un predicado SQL. Nada más:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">eval_result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">REFERENCES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;safety-es&amp;#39;, &amp;#39;support-helpfulness&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metric&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMERIC&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">judge_model&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">judged_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMPTZ&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metric&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">REPLACE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FUNCTION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">can_promote&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">current&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">RETURNS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$$&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">eval_result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">eval_result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metric&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">candidate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">current&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;safety-es&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;support-helpfulness&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;refusal-rate&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">98&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- tolerancia 2 %
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="err">$$&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LANGUAGE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">sql&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">STABLE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Una función SQL como gate. Aplicable desde el CI con &lt;code>psql -c &amp;quot;SELECT serve.can_promote('v4.2','v4.1')&amp;quot;&lt;/code> y un exit code 0/1. No hace falta un orquestador, no hace falta una UI específica. La auditoría queda en el log de Postgres.&lt;/p>
&lt;h2 id="vllm-multi-lora-el-deploy-es-un-post-http">vLLM multi-LoRA: el deploy es un POST HTTP&lt;/h2>
&lt;p>Hace dos años, desplegar un fine-tune nuevo era rotar pods de inferencia. Hoy es una llamada HTTP. vLLM 0.7+ soporta cargar y descargar adapters LoRA &lt;strong>en caliente&lt;/strong>, manteniendo varios residentes en VRAM y eligiendo el correcto por petición.&lt;/p>
&lt;p>Configuración del servidor:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-lora &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-loras &lt;span class="m">4&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-lora-rank &lt;span class="m">64&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --env &lt;span class="nv">VLLM_ALLOW_RUNTIME_LORA_UPDATING&lt;/span>&lt;span class="o">=&lt;/span>True
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Despliegue de un adapter nuevo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:8000/v1/load_lora_adapter &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;lora_name&amp;#34;: &amp;#34;support-es-v4.2&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;lora_path&amp;#34;: &amp;#34;/mnt/adapters/support-es-v4.2&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A partir de ese momento, las peticiones que incluyen &lt;code>&amp;quot;model&amp;quot;: &amp;quot;support-es-v4.2&amp;quot;&lt;/code> se sirven con ese adapter aplicado sobre el modelo base. El switch entre adapters tiene latencia despreciable (la investigación más reciente sobre Activated LoRA lleva esto a niveles donde el coste de cambio es invisible).&lt;/p>
&lt;p>Esto cambia la operación de forma sustancial. &lt;strong>El despliegue de un fine-tune nuevo deja de ser un evento de infraestructura para convertirse en un cambio de estado en Postgres&lt;/strong>. El router consulta la tabla &lt;code>serve.adapter&lt;/code>, ve que &lt;code>v4.2&lt;/code> está en &lt;code>canary&lt;/code> con &lt;code>traffic_pct=5&lt;/code>, y dirige el 5 % de peticiones al nuevo adapter. La ruta exacta del 5 % se decide con hashing determinístico del &lt;code>user_id&lt;/code> para que un mismo usuario siempre vea la misma variante (sticky):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- Sin tabla de asignación, sin estado adicional
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- el variant se calcula in-place en SQL o en el router:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">CASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hashtext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">experiment&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">traffic_pct&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">candidate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">THEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ELSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="k">current&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ab-con-tráfico-real-medir-o-vivir-engañado">A/B con tráfico real: medir o vivir engañado&lt;/h2>
&lt;p>Los eval gates miden contra benchmarks fijos. Eso es necesario pero insuficiente. La realidad solo se mide con tráfico real. Una vez el adapter está en canary, lo que importa son las &lt;strong>métricas online&lt;/strong> medidas sobre &lt;code>obs.inference_log&lt;/code> para cada variante:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">n&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">mean_score&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">STDDEV&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SQRT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sem&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ttft_ms&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ttft_avg&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">percentile_cont&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WITHIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ttft_ms&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ttft_p50&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">percentile_cont&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">95&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WITHIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ttft_ms&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ttft_p95&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">CASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_regen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">THEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ELSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">regen_rate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">experiment&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">interval&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;7 days&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lo que se mira: feedback explícito, latencia (TTFT, p50, p95), tasa de regeneración. Un adapter que sube el feedback medio pero también sube la tasa de regeneración es sospechoso —probablemente está respondiendo de forma más vistosa pero menos útil—. Un adapter que baja la latencia pero baja el feedback puede merecer estudio: puede que esté siendo más conciso de la cuenta.&lt;/p>
&lt;p>La promoción a &lt;code>prod&lt;/code> ocurre cuando, después de 24-72 horas en canary, el adapter candidato supera al actual en al menos una métrica clave sin degradar las demás. Otra vez: es un &lt;code>UPDATE&lt;/code> en Postgres.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;p>Bajemos a dos configuraciones representativas, una de iteración y otra de producción.&lt;/p>
&lt;h3 id="caso-1--rtx-4090-24-gb-para-iteración-de-desarrollo">Caso 1 — RTX 4090 (24 GB) para iteración de desarrollo&lt;/h3>
&lt;p>Una RTX 4090 con QLoRA 4-bit puede entrenar adapters sobre un modelo 8B sin sobresalto. El presupuesto de VRAM combina cuatro componentes; el &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> durante las evaluaciones intermedias no es despreciable y conviene reservarle margen explícito:&lt;/p>
&lt;pre tabindex="0">&lt;code>Modelo base 8B en 4-bit: ~5 GB
Activations + gradientes: ~8 GB (depende de batch y context)
Optimizer state (LoRA r=16): ~0.5 GB
KV cache durante eval: ~2 GB
Margen de seguridad: ~8 GB
&lt;/code>&lt;/pre>&lt;p>Tiempos típicos (estimación basada en benchmarks comunitarios; conviene medir con el lab antes de prometer):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dataset&lt;/th>
&lt;th>Técnica&lt;/th>
&lt;th style="text-align:right">Adapter rank&lt;/th>
&lt;th style="text-align:right">Tiempo aproximado&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1.000 ejemplos SFT&lt;/td>
&lt;td>LoRA r=16&lt;/td>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">20-40 min&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5.000 ejemplos SFT&lt;/td>
&lt;td>LoRA r=32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">2-4 h&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2.000 pares DPO&lt;/td>
&lt;td>LoRA r=16&lt;/td>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">1-2 h&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5.000 ejemplos KTO&lt;/td>
&lt;td>LoRA r=32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">3-5 h&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Esto pone el ciclo de iteración —cambio en dataset, retrain, eval, ver número— en franja de una &lt;strong>jornada de trabajo&lt;/strong>. Suficiente para validar hipótesis antes de mover nada al cluster de producción.&lt;/p>
&lt;h3 id="caso-2--cluster-4h100-sxm-320-gb-nvlink-para-producción">Caso 2 — Cluster 4×H100 SXM (320 GB, NVLink) para producción&lt;/h3>
&lt;p>Con un cluster de este orden todo el escenario cambia. Se puede:&lt;/p>
&lt;ul>
&lt;li>Entrenar &lt;strong>LoRA sobre 70B en BF16 sin quantización&lt;/strong> con tensor parallel = 4.&lt;/li>
&lt;li>Hacer &lt;strong>DPO completo con modelo de referencia residente&lt;/strong> cuando se cuantiza la referencia a FP8, o pasarse a &lt;strong>SimPO / ORPO&lt;/strong> que eliminan ese modelo intermedio y simplifican la planificación de VRAM (ver tabla de técnicas más arriba).&lt;/li>
&lt;li>Soportar &lt;strong>multi-tenant fine-tuning&lt;/strong>: varios adapters de clientes entrenándose en paralelo en pipelines lógicos separados, cada uno aislado en una partición distinta de Postgres con sus propias ACLs.&lt;/li>
&lt;li>Servir &lt;strong>multi-LoRA con &lt;code>--max-loras 8&lt;/code>&lt;/strong> sobre el modelo base sin que la concurrencia baje el throughput de forma perceptible.&lt;/li>
&lt;/ul>
&lt;p>La regla práctica de presupuesto: en horizonte de 12 meses, un equipo con este cluster puede ejecutar &lt;strong>~150-200 ciclos de fine-tuning continuo&lt;/strong> (training + eval + canary + promoción o descarte) si la disciplina del dataset y de los eval gates es estricta. Si no lo es, ejecutará el doble pero con la mitad de utilidad.&lt;/p>
&lt;h2 id="posición-dentro-de-la-arquitectura-lo-que-cubre-este-artículo-y-lo-que-no">Posición dentro de la arquitectura: lo que cubre este artículo y lo que no&lt;/h2>
&lt;p>Para situar el alcance: el ciclo dibujado al principio tiene siete cajas, todas ellas cubiertas aquí en su mecánica. Quedan &lt;strong>deliberadamente fuera&lt;/strong> tres capas transversales que son las que terminan separando un pipeline que funciona técnicamente de uno que sobrevive a una auditoría:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Provenance criptográfico y trazabilidad.&lt;/strong> Hemos mencionado &lt;code>dataset_snapshot&lt;/code> y &lt;code>pg_audit&lt;/code>, pero la mecánica completa —el &lt;code>set_hash&lt;/code> sobre los ejemplos, la integración con EU AI Act, el &lt;code>query_sql&lt;/code> congelado como prueba de qué entrenó al modelo— da para análisis entero.&lt;/li>
&lt;li>&lt;strong>Calibración del juez.&lt;/strong> Hemos asumido que LLM-as-judge funciona. Hace falta calibrarlo contra rúbrica humana en, al menos, 100 casos por suite crítica antes de fiarse. Sin esa calibración, los eval gates son teatro.&lt;/li>
&lt;li>&lt;strong>El problema del olvido.&lt;/strong> ¿Qué pasa si un usuario ejerce su derecho al olvido GDPR y sus interacciones formaron parte del dataset de un adapter ya en producción? No hay solución limpia. Hay opciones —retrain incremental, machine unlearning a nivel de muestra, negative LoRA— y conviene conocerlas antes de que un cliente pregunte.&lt;/li>
&lt;/ol>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Provenance criptográfico sobre Postgres&lt;/strong>: cómo &lt;code>set_hash&lt;/code> y &lt;code>query_sql&lt;/code> congelado componen una cadena de custodia auditable bajo EU AI Act.&lt;/li>
&lt;li>&lt;strong>Judge calibration honesta&lt;/strong>: por qué &lt;code>score &amp;gt; 0.85&lt;/code> no significa nada sin baseline humana, y cómo construir esa baseline sin que cueste un mes de trabajo.&lt;/li>
&lt;li>&lt;strong>El problema del olvido en adapters&lt;/strong>: machine unlearning a nivel de muestra, retrain incremental y otras técnicas para responder a GDPR sin tirar el adapter.&lt;/li>
&lt;li>&lt;strong>Online DPO y aprendizaje continuo on-policy&lt;/strong>: estado de la investigación 2026 (Fast-Slow Chasing, RLOO, iterative on-policy) y por qué todavía no es producción.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> — los fundamentos del cache que entra en juego en cada eval intermedia del entrenamiento y en cada despliegue del adapter resultante.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode en pods especializados&lt;/a> — el patrón de serving al que se conecta el multi-LoRA hot-swap descrito aquí: cada pod especializado carga sus adapters por separado.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Hu et al., &lt;em>LoRA: Low-Rank Adaptation of Large Language Models&lt;/em> (ICLR 2022).&lt;/li>
&lt;li>Dettmers et al., &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em> (NeurIPS 2023).&lt;/li>
&lt;li>Rafailov et al., &lt;em>Direct Preference Optimization: Your Language Model is Secretly a Reward Model&lt;/em> (NeurIPS 2023).&lt;/li>
&lt;li>Meng, Xia, Chen, &lt;em>SimPO: Simple Preference Optimization with a Reference-Free Reward&lt;/em> (NeurIPS 2024).&lt;/li>
&lt;li>Hong et al., &lt;em>ORPO: Monolithic Preference Optimization without Reference Model&lt;/em> (2024).&lt;/li>
&lt;li>Ethayarajh et al., &lt;em>KTO: Model Alignment as Prospect Theoretic Optimization&lt;/em> (2024).&lt;/li>
&lt;li>Kwon et al., &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> (SOSP 2023) — vLLM original.&lt;/li>
&lt;li>Documentación oficial de vLLM Multi-LoRA: &lt;a href="https://docs.vllm.ai/en/stable/features/lora/">https://docs.vllm.ai/en/stable/features/lora/&lt;/a>.&lt;/li>
&lt;li>Documentación oficial de pgvector 0.8: &lt;a href="https://github.com/pgvector/pgvector">https://github.com/pgvector/pgvector&lt;/a>.&lt;/li>
&lt;li>TRL (HuggingFace) docs: &lt;a href="https://huggingface.co/docs/trl">https://huggingface.co/docs/trl&lt;/a>.&lt;/li>
&lt;li>EU AI Act, texto consolidado y calendario de aplicación: &lt;a href="https://artificialintelligenceact.eu/">https://artificialintelligenceact.eu/&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>