<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Peft on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/peft/</link><description>Recent content in Peft on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Tue, 09 Jun 2026 03:00:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/peft/index.xml" rel="self" type="application/rss+xml"/><item><title>Runbook QLoRA: del dataset al adapter servido en multi-LoRA (procedimiento operativo)</title><link>https://blog.lo0.es/posts/qlora-runbook-fine-tuning-serving/</link><pubDate>Tue, 09 Jun 2026 03:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/qlora-runbook-fine-tuning-serving/</guid><description>&lt;blockquote>
&lt;p>Este es el &lt;strong>compañero operativo&lt;/strong> de &lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">QLoRA y multi-LoRA al límite en modelos pequeños&lt;/a>. Aquel post desmonta el &lt;em>porqué&lt;/em> —NF4, doble cuantización, paged optimizers, la matemática del adapter—; este es el &lt;em>cómo&lt;/em>, con comandos que se copian y pegan. Si no has leído el de fundamentos, léelo antes: aquí damos por sabido qué es un adapter, por qué el base vive en 4-bit y por qué el gradiente solo toca el adapter.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un procedimiento reproducible en cinco fases: &lt;strong>(1)&lt;/strong> fijar el entorno con versiones pineadas; &lt;strong>(2)&lt;/strong> preparar el dataset en formato chat; &lt;strong>(3)&lt;/strong> entrenar el adapter QLoRA con TRL + PEFT en una &lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong> usando gradient checkpointing, gradient accumulation y &lt;code>paged_adamw_8bit&lt;/code>; &lt;strong>(4)&lt;/strong> validar y versionar el adapter como artefacto de &lt;strong>megabytes&lt;/strong>; &lt;strong>(5)&lt;/strong> servirlo en &lt;strong>vLLM&lt;/strong> con &lt;code>--enable-lora&lt;/code>, cargándolo en caliente sin reiniciar el servidor y resolviéndolo desde almacenamiento de objetos. Todo on-premise, en hardware de consumo, sin sacar un dato del perímetro. Lo que sigue son los comandos exactos y el presupuesto de memoria que separa &amp;ldquo;cabe&amp;rdquo; de &amp;ldquo;OOM&amp;rdquo;.&lt;/p>
&lt;h2 id="el-flujo-de-extremo-a-extremo">El flujo de extremo a extremo&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline operativo QLoRA: dataset, entrenamiento, artefacto adapter, registro, serving vLLM">
&lt;defs>&lt;marker id="rm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;rect x="14" y="74" width="118" height="52" rx="7" fill="#eef2f6" stroke="currentColor" stroke-width="1.4"/>
&lt;text x="73" y="98" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="currentColor">1 · Dataset&lt;/text>
&lt;text x="73" y="114" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#555">JSONL chat&lt;/text>
&lt;rect x="172" y="74" width="118" height="52" rx="7" fill="#fff4d6" stroke="#a48000" stroke-width="1.6"/>
&lt;text x="231" y="94" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#7a5e00">2 · Entrenar&lt;/text>
&lt;text x="231" y="110" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">TRL+PEFT · 4090&lt;/text>
&lt;text x="231" y="122" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">NF4 · paged_adamw&lt;/text>
&lt;rect x="330" y="74" width="118" height="52" rx="7" fill="#fffbe9" stroke="#a48000" stroke-width="1.4"/>
&lt;text x="389" y="94" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#7a5e00">3 · Adapter&lt;/text>
&lt;text x="389" y="110" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">~17 MB&lt;/text>
&lt;text x="389" y="122" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">safetensors&lt;/text>
&lt;rect x="488" y="74" width="118" height="52" rx="7" fill="#e6d9f2" stroke="#5a2db0" stroke-width="1.4"/>
&lt;text x="547" y="94" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">4 · Registro&lt;/text>
&lt;text x="547" y="110" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">MinIO / S3&lt;/text>
&lt;text x="547" y="122" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">versionado + sha256&lt;/text>
&lt;rect x="646" y="74" width="118" height="52" rx="7" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.6"/>
&lt;text x="705" y="94" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">5 · Servir&lt;/text>
&lt;text x="705" y="110" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">vLLM --enable-lora&lt;/text>
&lt;text x="705" y="122" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">carga en caliente&lt;/text>
&lt;path d="M132,100 L170,100" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm)"/>
&lt;path d="M290,100 L328,100" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm)"/>
&lt;path d="M448,100 L486,100" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm)"/>
&lt;path d="M606,100 L644,100" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm)"/>
&lt;text x="389" y="36" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="currentColor">Productor (4090) ───────────────▶ Consumidor (4090 o cluster)&lt;/text>
&lt;path d="M231,150 L231,168 L547,168 L547,150" stroke="#999" stroke-width="1.2" fill="none" stroke-dasharray="4 3"/>
&lt;text x="389" y="185" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#555">el mismo equipo puede ser productor y consumidor&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="fase-0--entorno-y-versiones">Fase 0 — Entorno y versiones&lt;/h2>
&lt;p>QLoRA es sensible a las versiones de &lt;code>bitsandbytes&lt;/code>, &lt;code>transformers&lt;/code>, &lt;code>peft&lt;/code> y &lt;code>trl&lt;/code>: combinaciones desalineadas dan errores de dequant o adapters que no cargan en vLLM. Fija el entorno y no lo toques a mitad de campaña. Versiones de referencia a junio de 2026 (verifica las concretas de tu índice; el pin exacto importa menos que la coherencia entre ellas):&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">python -m venv .venv &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">source&lt;/span> .venv/bin/activate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install --upgrade pip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Entrenamiento (productor)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install &lt;span class="s2">&amp;#34;torch&amp;gt;=2.4&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> &lt;span class="s2">&amp;#34;transformers&amp;gt;=4.50&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> &lt;span class="s2">&amp;#34;peft&amp;gt;=0.14&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> &lt;span class="s2">&amp;#34;trl&amp;gt;=0.15&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> &lt;span class="s2">&amp;#34;bitsandbytes&amp;gt;=0.45&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> &lt;span class="s2">&amp;#34;accelerate&amp;gt;=1.2&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> &lt;span class="s2">&amp;#34;datasets&amp;gt;=3.2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Serving (consumidor) — en su propio entorno/imagen&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install &lt;span class="s2">&amp;#34;vllm&amp;gt;=0.8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Qué hace cada pieza y por qué está pineada:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Paquete&lt;/th>
&lt;th>Rol en el flujo&lt;/th>
&lt;th>Por qué la versión importa&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>torch&lt;/code>&lt;/td>
&lt;td>runtime de tensores y kernels CUDA&lt;/td>
&lt;td>el ABI de CUDA tiene que casar con el driver y con &lt;code>bitsandbytes&lt;/code>; un salto mayor rompe los kernels 4-bit.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>transformers&lt;/code>&lt;/td>
&lt;td>carga el base, el tokenizer y el &lt;code>chat_template&lt;/code>&lt;/td>
&lt;td>tiene que &lt;strong>conocer la arquitectura&lt;/strong> del SLM que uses; un modelo nuevo necesita una versión que lo soporte.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>peft&lt;/code>&lt;/td>
&lt;td>implementa LoRA/QLoRA: inyecta las matrices &lt;code>A,B&lt;/code> y escribe el &lt;code>adapter_config.json&lt;/code>&lt;/td>
&lt;td>ese &lt;code>adapter_config.json&lt;/code> es el que &lt;strong>vLLM lee&lt;/strong> al servir; versiones viejas escriben campos que el serving no entiende.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>trl&lt;/code>&lt;/td>
&lt;td>el &lt;code>SFTTrainer&lt;/code>: el bucle de entrenamiento supervisado&lt;/td>
&lt;td>integra &lt;code>peft&lt;/code> de forma nativa; su API (&lt;code>SFTConfig&lt;/code>) cambia entre versiones, de ahí el pin.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>bitsandbytes&lt;/code>&lt;/td>
&lt;td>la cuantización NF4 y el &lt;code>paged_adamw_8bit&lt;/code>&lt;/td>
&lt;td>&lt;strong>la pieza más sensible&lt;/strong>: un binario mal compilado da dequant corrupto o cuelga al primer paso.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>accelerate&lt;/code>&lt;/td>
&lt;td>orquesta dispositivo, precisión mixta y &lt;code>device_map&lt;/code>&lt;/td>
&lt;td>backend silencioso de casi todo; desalinearlo con &lt;code>transformers&lt;/code> da errores crípticos.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>datasets&lt;/code>&lt;/td>
&lt;td>carga el JSONL (y permite streaming si el corpus es grande)&lt;/td>
&lt;td>poco sensible; cualquier 3.x reciente sirve.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm&lt;/code>&lt;/td>
&lt;td>el serving multi-LoRA&lt;/td>
&lt;td>&lt;strong>entorno o imagen aparte&lt;/strong>: no mezcles su stack con el &lt;code>bitsandbytes&lt;/code> de entrenamiento.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La regla de oro: &lt;strong>coherencia entre los cuatro de arriba&lt;/strong> (&lt;code>transformers&lt;/code>, &lt;code>peft&lt;/code>, &lt;code>trl&lt;/code>, &lt;code>bitsandbytes&lt;/code>) pesa más que el número exacto de cada uno. Fíjalos al empezar una campaña y no los muevas hasta cerrarla.&lt;/p>
&lt;p>Comprueba que la GPU y CUDA están sanos antes de empezar; un &lt;code>bitsandbytes&lt;/code> mal compilado se manifiesta tarde:&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">python -c &lt;span class="s2">&amp;#34;import torch, bitsandbytes; print(torch.cuda.get_device_name(0), torch.cuda.is_available())&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nvidia-smi --query-gpu&lt;span class="o">=&lt;/span>name,memory.total,driver_version --format&lt;span class="o">=&lt;/span>csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para 100 % soberanía: descarga el base una vez desde tu mirror interno de Hugging Face (o un MinIO con los pesos) y exporta &lt;code>HF_HOME&lt;/code> a un volumen local. Nada de este flujo necesita salir del perímetro.&lt;/p>
&lt;h2 id="fase-1--preparar-el-dataset">Fase 1 — Preparar el dataset&lt;/h2>
&lt;p>El formato canónico para una tarea conversacional es JSONL, una conversación por línea, con la plantilla de chat del modelo. No inventes un formato propio: usa el &lt;code>chat_template&lt;/code> del tokenizer del base, porque cualquier desajuste entre cómo entrenas y cómo sirves degrada la calidad de forma silenciosa.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-jsonl" data-lang="jsonl">{&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;system&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Eres un asistente de soporte de redes.&amp;#34;},{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;El AP del ala norte no levanta tras el corte.&amp;#34;},{&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Confirma primero el PoE del puerto...&amp;#34;}]}
{&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Genera el cambio de VLAN para el cliente 42.&amp;#34;},{&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;interface GigabitEthernet0/3\n switchport access vlan 42...&amp;#34;}]}
&lt;/code>&lt;/pre>&lt;p>Qué es cada campo y por qué:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Campo&lt;/th>
&lt;th>Qué es&lt;/th>
&lt;th>Nota operativa&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>messages&lt;/code>&lt;/td>
&lt;td>la conversación completa, lista de turnos&lt;/td>
&lt;td>una conversación por línea JSONL; es lo que &lt;code>apply_chat_template&lt;/code> convierte en tokens.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>role&lt;/code>&lt;/td>
&lt;td>quién habla: &lt;code>system&lt;/code>, &lt;code>user&lt;/code>, &lt;code>assistant&lt;/code>&lt;/td>
&lt;td>el adapter aprende a producir los turnos &lt;code>assistant&lt;/code>; los &lt;code>user&lt;/code>/&lt;code>system&lt;/code> son contexto, no objetivo.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>content&lt;/code>&lt;/td>
&lt;td>el texto del turno&lt;/td>
&lt;td>el &lt;code>system&lt;/code> fija la persona/tarea; mantenlo &lt;strong>idéntico&lt;/strong> al que usarás en producción o el adapter se desalinea.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Reglas operativas que ahorran disgustos: cuida la &lt;strong>proporción de ejemplos&lt;/strong> (un dataset de tarea estrecha bien curado de 2.000–20.000 ejemplos rinde más que 200.000 ruidosos), &lt;strong>deduplica&lt;/strong>, y reserva un 5–10 % como split de validación que NO entra en el entrenamiento. La construcción del corpus a partir de señal de producción la cubre &lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle&lt;/a>.&lt;/p>
&lt;h2 id="fase-2--el-script-de-entrenamiento">Fase 2 — El script de entrenamiento&lt;/h2>
&lt;p>Script mínimo y completo con TRL + PEFT. Entrena un adapter r=8 sobre un SLM de 8B cuantizado a NF4. Cada bloque tiene su porqué comentado.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># train_qlora.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">torch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datasets&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">load_dataset&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">transformers&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">AutoTokenizer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">AutoModelForCausalLM&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">BitsAndBytesConfig&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">peft&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">LoraConfig&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">trl&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">SFTConfig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SFTTrainer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BASE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Qwen/Qwen3-8B&amp;#34;&lt;/span> &lt;span class="c1"># o el SLM que sirvas; usa SIEMPRE el mismo en train y serve&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">OUT&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;adapters/soporte-redes-v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1) Base congelado y cuantizado a 4-bit NF4 con doble cuantización&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">bnb&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">BitsAndBytesConfig&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">load_in_4bit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">bnb_4bit_quant_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;nf4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># NormalFloat, cuantil-óptimo para pesos gaussianos&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">bnb_4bit_use_double_quant&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># cuantiza las constantes de escala -&amp;gt; ~0.37 bits/param menos&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">bnb_4bit_compute_dtype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">torch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bfloat16&lt;/span> &lt;span class="c1"># los matmuls se hacen en BF16 tras dequant al vuelo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">tok&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">AutoTokenizer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_pretrained&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BASE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">AutoModelForCausalLM&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_pretrained&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BASE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">quantization_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">bnb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">torch_dtype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">torch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bfloat16&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">device_map&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2) El adapter: rank bajo, solo proyecciones de atención (agresivo). Sube target_modules si el eval lo pide.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">peft_cfg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">LoraConfig&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">r&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lora_alpha&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lora_dropout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.05&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bias&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">task_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;CAUSAL_LM&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">target_modules&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;q_proj&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;k_proj&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;v_proj&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;o_proj&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ds&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_dataset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data_files&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;train&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;data/train.jsonl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;eval&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;data/eval.jsonl&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3) Config de entrenamiento pensada para caber en 24 GB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">SFTConfig&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">output_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">OUT&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">per_device_train_batch_size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># batch real pequeño&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gradient_accumulation_steps&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># batch EFECTIVO = 1*16 = 16, sin pagar su VRAM de golpe&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gradient_checkpointing&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># recomputa activaciones en backward: cambia compute por memoria&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">optim&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;paged_adamw_8bit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># paged optimizer: el airbag contra los picos de VRAM&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">learning_rate&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">2e-4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lr_scheduler_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;cosine&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">warmup_ratio&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.03&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">num_train_epochs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bf16&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">max_length&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2048&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># acota la secuencia: las activaciones escalan con ella&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">logging_steps&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">eval_strategy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;steps&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">eval_steps&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">save_steps&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">report_to&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">trainer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">SFTTrainer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">cfg&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">peft_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">peft_cfg&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">train_dataset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ds&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;train&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">eval_dataset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ds&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;eval&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">processing_class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">tok&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">trainer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">trainer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">save_model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OUT&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># guarda SOLO el adapter (MB), no el base&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bitsandbytesconfig--cómo-se-cuantiza-el-base">&lt;code>BitsAndBytesConfig&lt;/code> — cómo se cuantiza el base&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Opción&lt;/th>
&lt;th>Qué hace&lt;/th>
&lt;th>Por qué este valor / cuándo cambiarlo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>load_in_4bit=True&lt;/code>&lt;/td>
&lt;td>carga los pesos del base en 4-bit&lt;/td>
&lt;td>es la base de QLoRA: sin esto el 8B no cabe ni para entrenar.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>bnb_4bit_quant_type=&amp;quot;nf4&amp;quot;&lt;/code>&lt;/td>
&lt;td>usa el formato NF4 (cuantil-óptimo para pesos gaussianos)&lt;/td>
&lt;td>existe &lt;code>&amp;quot;fp4&amp;quot;&lt;/code>, pero NF4 rinde mejor en pesos de transformer; deja NF4.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>bnb_4bit_use_double_quant=True&lt;/code>&lt;/td>
&lt;td>cuantiza las propias constantes de escala&lt;/td>
&lt;td>ahorra ~0.37 bits/param (cientos de MB en un 8B); el margen que separa &amp;ldquo;cabe&amp;rdquo; de &amp;ldquo;OOM&amp;rdquo;. Déjalo en &lt;code>True&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>bnb_4bit_compute_dtype=torch.bfloat16&lt;/code>&lt;/td>
&lt;td>precisión del matmul tras deshacer la cuantización al vuelo&lt;/td>
&lt;td>BF16 en Ada/Hopper (4090, H100); usa &lt;code>float16&lt;/code> solo en GPUs sin BF16.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="loraconfig--la-forma-del-adapter">&lt;code>LoraConfig&lt;/code> — la forma del adapter&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Opción&lt;/th>
&lt;th>Qué hace&lt;/th>
&lt;th>Por qué este valor / cuándo cambiarlo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>r=8&lt;/code>&lt;/td>
&lt;td>rank del adapter: su capacidad de corrección&lt;/td>
&lt;td>4-8 para tarea estrecha (agresivo); súbelo a 16-64 solo si el eval muestra underfitting.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>lora_alpha=16&lt;/code>&lt;/td>
&lt;td>factor de escala del delta (efectivo &lt;code>α/r&lt;/code>)&lt;/td>
&lt;td>convención común &lt;code>α=2r&lt;/code>; modula cuánto &amp;ldquo;pesa&amp;rdquo; el adapter sobre el base.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>lora_dropout=0.05&lt;/code>&lt;/td>
&lt;td>regularización sobre el adapter&lt;/td>
&lt;td>0.05-0.1 con datasets pequeños (evita overfit); 0 si el corpus es grande.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>bias=&amp;quot;none&amp;quot;&lt;/code>&lt;/td>
&lt;td>no entrena los términos de bias&lt;/td>
&lt;td>&lt;code>&amp;quot;none&amp;quot;&lt;/code> es el estándar; &lt;code>&amp;quot;all&amp;quot;&lt;/code>/&lt;code>&amp;quot;lora_only&amp;quot;&lt;/code> rara vez aportan y cuestan params.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>task_type=&amp;quot;CAUSAL_LM&amp;quot;&lt;/code>&lt;/td>
&lt;td>tipo de objetivo/cabeza&lt;/td>
&lt;td>fijo para un LLM generativo.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>target_modules=[q,k,v,o]&lt;/code>&lt;/td>
&lt;td>qué matrices reciben adapter&lt;/td>
&lt;td>solo atención = barato y agresivo; añade &lt;code>gate_proj&lt;/code>/&lt;code>up_proj&lt;/code>/&lt;code>down_proj&lt;/code> (MLP) si la tarea exige reescribir más comportamiento y el eval lo pide.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="sftconfig--el-presupuesto-de-memoria-y-el-bucle">&lt;code>SFTConfig&lt;/code> — el presupuesto de memoria y el bucle&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Opción&lt;/th>
&lt;th>Qué hace&lt;/th>
&lt;th>Por qué este valor / cuándo cambiarlo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>per_device_train_batch_size=1&lt;/code>&lt;/td>
&lt;td>microbatch por GPU&lt;/td>
&lt;td>1 en 24 GB; el batch real lo construye &lt;code>gradient_accumulation_steps&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gradient_accumulation_steps=16&lt;/code>&lt;/td>
&lt;td>acumula 16 microbatches antes de actualizar&lt;/td>
&lt;td>batch &lt;strong>efectivo&lt;/strong> = 1×16 = 16 sin pagar su VRAM de golpe; súbelo si bajas la secuencia y quieres más batch efectivo.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gradient_checkpointing=True&lt;/code>&lt;/td>
&lt;td>recomputa activaciones en el backward en vez de guardarlas&lt;/td>
&lt;td>imprescindible en 4090: ~20-30 % más lento a cambio de mucha menos VRAM.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>optim=&amp;quot;paged_adamw_8bit&amp;quot;&lt;/code>&lt;/td>
&lt;td>optimizer Adam en 8-bit + estados paginables a RAM&lt;/td>
&lt;td>menos VRAM de estados &lt;strong>y&lt;/strong> el airbag que evita el OOM en los picos.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>learning_rate=2e-4&lt;/code>&lt;/td>
&lt;td>tasa de aprendizaje del adapter&lt;/td>
&lt;td>1e-4–3e-4 es el rango típico de QLoRA; los adapters toleran LR más alto que un full fine-tune.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>lr_scheduler_type=&amp;quot;cosine&amp;quot;&lt;/code>&lt;/td>
&lt;td>curva de decaimiento del LR&lt;/td>
&lt;td>&lt;code>cosine&lt;/code> o &lt;code>linear&lt;/code>; cosine suele dar una bajada suave al final.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>warmup_ratio=0.03&lt;/code>&lt;/td>
&lt;td>calienta el LR el primer 3 % de pasos&lt;/td>
&lt;td>evita la inestabilidad de los primeros steps.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>num_train_epochs=3&lt;/code>&lt;/td>
&lt;td>pasadas completas al dataset&lt;/td>
&lt;td>1-3; vigila la &lt;em>eval loss&lt;/em> para no sobreajustar.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>bf16=True&lt;/code>&lt;/td>
&lt;td>precisión de cómputo y del adapter&lt;/td>
&lt;td>BF16 en Ada/Hopper; &lt;code>fp16=True&lt;/code> si tu GPU no tiene BF16.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>max_length=2048&lt;/code>&lt;/td>
&lt;td>longitud máxima de secuencia&lt;/td>
&lt;td>&lt;strong>la palanca #1 de VRAM&lt;/strong> de activaciones: acórtala lo primero si hay OOM.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>eval_strategy&lt;/code>/&lt;code>eval_steps&lt;/code>/&lt;code>save_steps&lt;/code>&lt;/td>
&lt;td>cadencia de validación y checkpoint&lt;/td>
&lt;td>ajústalas al tamaño del dataset; evaluar a menudo cuesta tiempo.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Las cuatro piezas que hacen que quepa en una 4090 son: &lt;code>per_device_train_batch_size=1&lt;/code> + &lt;code>gradient_accumulation_steps&lt;/code> (batch efectivo grande sin su coste de memoria de golpe), &lt;code>gradient_checkpointing=True&lt;/code> (recomputar activaciones en lugar de guardarlas) y &lt;code>optim=&amp;quot;paged_adamw_8bit&amp;quot;&lt;/code> (paginar estados a RAM en los picos). Quita cualquiera de las tres con secuencias largas y verás el OOM.&lt;/p>
&lt;p>Alternativa declarativa con &lt;strong>Axolotl&lt;/strong> si prefieres YAML sobre Python (mismo resultado):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">base_model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Qwen/Qwen3-8B&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="nt">load_in_4bit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">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="nt">adapter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qlora&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="nt">lora_r&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&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="nt">lora_alpha&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">16&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="nt">lora_target_modules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">q_proj, k_proj, v_proj, o_proj]&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="nt">sequence_len&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2048&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="nt">micro_batch_size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">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="nt">gradient_accumulation_steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">16&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="nt">gradient_checkpointing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">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="nt">optimizer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">paged_adamw_8bit&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="nt">learning_rate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0002&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="nt">num_epochs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&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="nt">bf16&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">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="nt">datasets&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="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data/train.jsonl&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="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chat_template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="fase-3--lanzar-y-monitorizar">Fase 3 — Lanzar y monitorizar&lt;/h2>
&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">&lt;span class="c1"># Lanzamiento simple en una GPU&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python train_qlora.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># En otra terminal: vigila la VRAM. Si se acerca al techo, baja max_length o sube grad accumulation.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">watch -n &lt;span class="m">2&lt;/span> nvidia-smi --query-gpu&lt;span class="o">=&lt;/span>memory.used,memory.total,utilization.gpu --format&lt;span class="o">=&lt;/span>csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Presupuesto aproximado de VRAM al entrenar el 8B en la 4090, y qué tocar cuando aprieta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Componente&lt;/th>
&lt;th style="text-align:right">VRAM aprox.&lt;/th>
&lt;th>Palanca si hay OOM&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Base 8B NF4 (congelado)&lt;/td>
&lt;td style="text-align:right">~4.0 GB&lt;/td>
&lt;td>— (fijo)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adapter + grad + estados Adam&lt;/td>
&lt;td style="text-align:right">~0.3–0.7 GB&lt;/td>
&lt;td>bajar &lt;code>r&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Activaciones (batch × secuencia)&lt;/td>
&lt;td style="text-align:right">~6–14 GB&lt;/td>
&lt;td>bajar &lt;code>max_length&lt;/code>, &lt;code>batch_size&lt;/code>; subir &lt;code>grad_accum&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Buffers dequant / workspace&lt;/td>
&lt;td style="text-align:right">~1–2 GB&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tabla de remedios rápidos de OOM, en orden de coste: &lt;strong>(1)&lt;/strong> baja &lt;code>max_length&lt;/code>; &lt;strong>(2)&lt;/strong> confirma &lt;code>gradient_checkpointing=True&lt;/code>; &lt;strong>(3)&lt;/strong> sube &lt;code>gradient_accumulation_steps&lt;/code> y baja &lt;code>per_device_train_batch_size&lt;/code> a 1; &lt;strong>(4)&lt;/strong> usa &lt;code>paged_adamw_8bit&lt;/code> (ya en el script); &lt;strong>(5)&lt;/strong> como último recurso baja &lt;code>r&lt;/code>. Si tras todo eso no cabe, la secuencia o el modelo son demasiado grandes para 24 GB: o acotas, o subes de hardware.&lt;/p>
&lt;h2 id="fase-4--validar-el-adapter">Fase 4 — Validar el adapter&lt;/h2>
&lt;p>Nunca promociones un adapter por la &lt;em>training loss&lt;/em>. Mide contra el split de validación reservado y contra un puñado de prompts reales.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># quick_eval.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">torch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">transformers&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">AutoTokenizer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">AutoModelForCausalLM&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">BitsAndBytesConfig&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">peft&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">PeftModel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">bnb&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">BitsAndBytesConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">load_in_4bit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bnb_4bit_quant_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;nf4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">bnb_4bit_use_double_quant&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bnb_4bit_compute_dtype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">torch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bfloat16&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">tok&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">AutoTokenizer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_pretrained&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen3-8B&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">base&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">AutoModelForCausalLM&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_pretrained&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen3-8B&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">quantization_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">bnb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">device_map&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PeftModel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_pretrained&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">base&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;adapters/soporte-redes-v1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># base + adapter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">msgs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;El AP del ala norte no levanta tras el corte.&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ids&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tok&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply_chat_template&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">msgs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">add_generation_prompt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">return_tensors&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;pt&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tok&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">decode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">generate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ids&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">max_new_tokens&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">256&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">skip_special_tokens&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para un veredicto serio, pasa el adapter por tu suite de evals (la capa que describe &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals LLM&lt;/a>) y compara contra el base &lt;strong>sin&lt;/strong> adapter y contra la versión anterior del adapter. Promociona solo si gana en la métrica de la tarea sin regresar en seguridad/formato.&lt;/p>
&lt;h2 id="fase-5--versionar-el-adapter-como-artefacto">Fase 5 — Versionar el adapter como artefacto&lt;/h2>
&lt;p>El adapter es un par de ficheros de MB (&lt;code>adapter_model.safetensors&lt;/code> + &lt;code>adapter_config.json&lt;/code>). Trátalo como un artefacto versionado, firmado y trazable, no como un fichero suelto.&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">&lt;span class="c1"># Checksum reproducible + subida a almacenamiento de objetos interno (MinIO/S3)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sha256sum adapters/soporte-redes-v1/adapter_model.safetensors &amp;gt; adapters/soporte-redes-v1/SHA256
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aws --endpoint-url https://minio.interno s3 cp &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> adapters/soporte-redes-v1/ s3://adapters/soporte-redes/v1/ --recursive
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Convención que funciona: &lt;code>s3://adapters/&amp;lt;tarea-o-cliente&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code>. Inmutable por versión, con su &lt;code>SHA256&lt;/code>. Borrar un cliente es borrar un prefijo de MB, no reentrenar nada. Versionar 500 adapters cuesta lo que cuesta versionar 500 ficheros de configuración pesados.&lt;/p>
&lt;h2 id="fase-6--servir-en-multi-lora-con-vllm">Fase 6 — Servir en multi-LoRA con vLLM&lt;/h2>
&lt;p>El consumidor carga &lt;strong>un&lt;/strong> base compartido y aplica el delta del adapter por request. Arranque con adapters estáticos declarados:&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">&lt;span class="nv">VLLM_ALLOW_RUNTIME_LORA_UPDATING&lt;/span>&lt;span class="o">=&lt;/span>True &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>vllm serve Qwen/Qwen3-8B &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">8&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># nº máx de adapters DISTINTOS por batch (no el total cargable)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --max-lora-rank &lt;span class="m">8&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># = al rank máximo de tus adapters; no lo infles (gasta memoria)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --max-cpu-loras &lt;span class="m">64&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># adapters cacheados en RAM para swap rápido a VRAM&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --lora-modules soporte-redes&lt;span class="o">=&lt;/span>/srv/adapters/soporte-redes/v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cada flag, qué controla y cómo dimensionarlo:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Flag / variable&lt;/th>
&lt;th>Qué controla&lt;/th>
&lt;th>Cómo dimensionarlo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>--enable-lora&lt;/code>&lt;/td>
&lt;td>activa el soporte de adapters&lt;/td>
&lt;td>obligatorio; sin él, vLLM ignora cualquier &lt;code>model&lt;/code> que sea un adapter.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--max-loras 8&lt;/code>&lt;/td>
&lt;td>nº de adapters &lt;strong>distintos en un mismo batch&lt;/strong>&lt;/td>
&lt;td>más adapters por batch encarece los kernels SGMV; 8-32 es razonable. No es el total cargable.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--max-lora-rank 8&lt;/code>&lt;/td>
&lt;td>rank máximo que el servidor reserva&lt;/td>
&lt;td>ponlo &lt;strong>igual al rank real&lt;/strong> de tus adapters (8 aquí); inflarlo desperdicia VRAM y rendimiento.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--max-cpu-loras 64&lt;/code>&lt;/td>
&lt;td>adapters cacheados en RAM listos para paginar a VRAM&lt;/td>
&lt;td>≥ nº de adapters activos; es el &amp;ldquo;banquillo&amp;rdquo; desde el que se hace swap rápido.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--lora-modules name=path&lt;/code>&lt;/td>
&lt;td>declara adapters &lt;strong>estáticos&lt;/strong> al arrancar&lt;/td>
&lt;td>útil para los fijos; omítelo si todo va por carga dinámica/Resolver.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>VLLM_ALLOW_RUNTIME_LORA_UPDATING=True&lt;/code>&lt;/td>
&lt;td>habilita los endpoints de carga/descarga en caliente&lt;/td>
&lt;td>imprescindible para &lt;code>/v1/load_lora_adapter&lt;/code>; sin él, el servidor es estático.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>--max-loras&lt;/code> limita los adapters distintos &lt;strong>por batch&lt;/strong>, no cuántos puedes tener cargados; el grueso vive en CPU (&lt;code>--max-cpu-loras&lt;/code>) y se pagina a VRAM bajo demanda. Pon &lt;code>--max-lora-rank&lt;/code> al rank real (8 aquí): inflarlo desperdicia memoria y rendimiento. Las peticiones eligen adapter por el campo &lt;code>model&lt;/code>:&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 http://localhost:8000/v1/chat/completions -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&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;model&amp;#34;: &amp;#34;soporte-redes&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;El AP del ala norte no levanta tras el corte.&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;span class="line">&lt;span class="cl">&lt;span class="c1"># model:&amp;#34;Qwen/Qwen3-8B&amp;#34; (sin adapter) usa el base pelado en el mismo servidor&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Carga en caliente&lt;/strong> de un adapter nuevo sin reiniciar (gracias a &lt;code>VLLM_ALLOW_RUNTIME_LORA_UPDATING=True&lt;/code>):&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 -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&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;cliente-42&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;/srv/adapters/cliente-42/v3&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;span class="line">&lt;span class="cl">&lt;span class="c1"># y para liberar VRAM/CPU cuando un cliente queda inactivo:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:8000/v1/unload_lora_adapter -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;lora_name&amp;#34;:&amp;#34;cliente-42&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para multi-tenant a escala, evita declarar cientos de adapters a mano: el &lt;strong>LoRAResolver&lt;/strong> resuelve y carga el adapter desde almacenamiento local o S3 la primera vez que llega un &lt;code>model&lt;/code> desconocido, así el servidor se mantiene fino y los adapters se traen perezosamente desde tu MinIO. Los internals de &lt;em>cómo&lt;/em> se batchean miles de adapters concurrentes (kernels SGMV, unified paging, el gather/scatter heterogéneo) están en &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>; este runbook solo los enciende. Para exprimir el throughput de decode del base en una 4090, combina esto con lo de &lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">Optimizando el decode en vLLM&lt;/a>.&lt;/p>
&lt;h2 id="servir-multi-adapter-vs-fusionar-por-tarea">Servir multi-adapter vs fusionar por tarea&lt;/h2>
&lt;p>Dos arquitecturas de despliegue, y el procedimiento cambia:&lt;/p>
&lt;p>&lt;strong>Servir multi-LoRA (lo de arriba).&lt;/strong> Un base compartido + N adapters en caliente. Es el patrón soberano por defecto: footprint mínimo, aislamiento por cliente, hot-swap. Usa QLoRA estándar y no fusiones nada.&lt;/p>
&lt;p>&lt;strong>Fusionar por tarea.&lt;/strong> Si quieres un único artefacto cuantizado-y-adaptado por tarea (sin adapter en runtime), no fusiones un adapter QLoRA estándar en el base 4-bit: la fusión reintroduce precisión que NF4 no representa y al recuantizar pierdes parte de lo aprendido. Para ese caso entrena con &lt;strong>QA-LoRA&lt;/strong> (quantization-aware), que fusiona limpio sobre un base cuantizado. Es una decisión de arquitectura, no de calidad; el detalle conceptual está en el &lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">post de fundamentos&lt;/a>.&lt;/p>
&lt;h2 id="checklist-de-gotchas-operativos">Checklist de gotchas operativos&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Plantilla de chat coherente&lt;/strong> entre entrenamiento y serving. El desajuste más común y más silencioso: entrenas con un &lt;code>chat_template&lt;/code> y sirves con otro. Usa el del base en ambos lados.&lt;/li>
&lt;li>&lt;strong>Mismo base exacto&lt;/strong> (revisión incluida) en &lt;code>train&lt;/code> y &lt;code>serve&lt;/code>. Un adapter entrenado sobre &lt;code>Qwen3-8B&lt;/code> no es válido sobre otra revisión del modelo.&lt;/li>
&lt;li>&lt;strong>&lt;code>--max-lora-rank&lt;/code> ≥ rank de TODOS los adapters&lt;/strong> servidos juntos, pero no más: inflarlo gasta VRAM.&lt;/li>
&lt;li>&lt;strong>Presupuesto KV vs &lt;code>--max-loras&lt;/code>.&lt;/strong> El cuello en serving no son los adapters (MB), es el KV cache y la concurrencia; mira &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">Roofline invertido&lt;/a> para el régimen del SLM.&lt;/li>
&lt;li>&lt;strong>&lt;code>r&lt;/code> demasiado bajo&lt;/strong> = underfitting si la tarea exige reescribir mucho comportamiento. Sube &lt;code>r&lt;/code> solo si el eval lo pide.&lt;/li>
&lt;li>&lt;strong>No promociones por training loss.&lt;/strong> Valida contra split reservado + prompts reales + regresión de seguridad.&lt;/li>
&lt;li>&lt;strong>Versiona e inmutabiliza&lt;/strong> cada adapter con su &lt;code>SHA256&lt;/code>; nunca sobrescribas una versión servida.&lt;/li>
&lt;/ul>
&lt;h2 id="aplicado-a-la-infraestructura-on-premise">Aplicado a la infraestructura on-premise&lt;/h2>
&lt;p>En una &lt;strong>RTX 4090 (24 GB)&lt;/strong> el mismo equipo es productor y consumidor: entrenas el adapter de un cliente en horas y lo sirves en el mismo servidor sobre el base compartido. Es el caso canónico para demos multi-tenant y prototipos de plataforma.&lt;/p>
&lt;p>En un &lt;strong>cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)&lt;/strong> QLoRA deja de ser necesario para &lt;em>caber&lt;/em>, pero sirve para &lt;strong>paralelizar la producción&lt;/strong> (varios jobs de adapter a la vez) y para mantener el formato cuantizado consistente entre entrenamiento y un serving serio de cientos de adapters concurrentes. El base puede ir en FP8 nativo; la mecánica del runbook no cambia, solo la escala.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">QLoRA y multi-LoRA al límite en modelos pequeños&lt;/a> — el post de fundamentos: el porqué de NF4, doble cuantización, paged optimizers y la matemática del adapter. Este runbook es su cara ejecutable.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a> — los internals del consumidor que aquí solo encendemos: SGMV, unified paging, batching heterogéneo de miles de adapters.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">Optimizando el decode en vLLM&lt;/a> — cómo exprimir el throughput de decode del base sobre el que sirves los adapters en una 4090.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle feedback → dataset → adapter&lt;/a> — de dónde sale el dataset de la Fase 1.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals LLM: la capa después del tracing&lt;/a> — cómo validar el adapter de la Fase 4 con criterio, no con la training loss.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">Roofline invertido en modelos pequeños&lt;/a> — el régimen de rendimiento que explica por qué el cuello del serving es el KV cache, no los adapters.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cuantizacion-agresiva-sub-4-bit-ternario/">Cuantización agresiva: del 4-bit al ternario&lt;/a> — qué pasa con el base cuantizado por debajo de NF4 bajo el adapter.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Dettmers, T., Pagnoni, A., Holtzman, A., Zettlemoyer, L. &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em>. NeurIPS 2023. &lt;a href="https://arxiv.org/abs/2305.14314">https://arxiv.org/abs/2305.14314&lt;/a>&lt;/li>
&lt;li>Hugging Face TRL — PEFT integration (SFTTrainer + QLoRA): &lt;a href="https://huggingface.co/docs/trl/peft_integration">https://huggingface.co/docs/trl/peft_integration&lt;/a>&lt;/li>
&lt;li>Hugging Face PEFT: &lt;a href="https://github.com/huggingface/peft">https://github.com/huggingface/peft&lt;/a>&lt;/li>
&lt;li>bitsandbytes: &lt;a href="https://github.com/bitsandbytes-foundation/bitsandbytes">https://github.com/bitsandbytes-foundation/bitsandbytes&lt;/a>&lt;/li>
&lt;li>vLLM — LoRA Adapters (serving, carga dinámica, LoRAResolver): &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>Axolotl: &lt;a href="https://github.com/axolotl-ai-cloud/axolotl">https://github.com/axolotl-ai-cloud/axolotl&lt;/a>&lt;/li>
&lt;li>Xu, Y. et al. &lt;em>QA-LoRA: Quantization-Aware Low-Rank Adaptation&lt;/em>. ICLR 2024. &lt;a href="https://arxiv.org/abs/2309.14717">https://arxiv.org/abs/2309.14717&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>