<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Amx on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/amx/</link><description>Recent content in Amx on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 11 Jun 2026 03:20:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/amx/index.xml" rel="self" type="application/rss+xml"/><item><title>Llevar el RAG a la CPU: separar el plano de datos del plano de generación</title><link>https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/</link><pubDate>Thu, 11 Jun 2026 03:20:00 +0000</pubDate><guid>https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/</guid><description>&lt;blockquote>
&lt;p>Tercera pieza de una serie operativa sobre exprimir un cluster LLM on-premise genérico de &lt;strong>4×H100 SXM 80 GB&lt;/strong>. Las hermanas: &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">compartir una GPU entre cargas&lt;/a> (time-slicing, MPS, MIG) y &lt;a href="https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/">servir varios modelos en una GPU&lt;/a> (swap + sleep) atacan el reparto &lt;em>dentro&lt;/em> de la GPU. Este ataca el reparto &lt;em>fuera&lt;/em>: qué partes del RAG no tienen por qué tocar la GPU nunca. El cierre de la serie, el asistente soberano end-to-end (cuarta entrega, en preparación), monta el sistema completo donde estas piezas encajan.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un sistema RAG no es una cosa, son &lt;strong>tres fases con perfiles de cómputo opuestos&lt;/strong>, y meterlas todas en la GPU &amp;ldquo;porque es IA&amp;rdquo; es un error de reparto. (1) &lt;strong>Construcción/ingesta&lt;/strong> —embeber el corpus y construir el índice— es trabajo &lt;strong>batch, throughput-bound, sin SLA de latencia&lt;/strong>: su sitio natural es la CPU. (2) &lt;strong>Retrieval en tiempo de consulta&lt;/strong> —embeber la query, búsqueda HNSW, fusión RRF, rerank ligero— es &lt;strong>mayoritariamente CPU&lt;/strong>, con matices solo en el rerank pesado; la búsqueda vectorial &lt;strong>siempre&lt;/strong> fue CPU, incluso en stacks que se venden como &amp;ldquo;GPU&amp;rdquo;. (3) &lt;strong>Generación&lt;/strong> —el LLM produciendo la respuesta— es &lt;strong>latency-bound&lt;/strong> y ahí la GPU es irremplazable: un 7B en CPU da un &lt;em>time to first token&lt;/em> de &lt;strong>segundos&lt;/strong>, inaceptable para chat. La clave técnica de por qué (1) y (2) caben en CPU: el embedder no es un LLM. &lt;code>bge-m3&lt;/code> son &lt;strong>~568M parámetros&lt;/strong> (un encoder XLM-RoBERTa), no 7B+; en int8 ocupa ~580 MB y activa rutas de cómputo entero rápidas (Intel &lt;strong>AVX-512 + VNNI + AMX&lt;/strong> en Xeon de 4ª gen en adelante; &lt;strong>NEON SDOT/UDOT&lt;/strong> en ARM). Hay runtimes listos: &lt;strong>TEI&lt;/strong> con backend CPU (mismo API OpenAI &lt;code>/v1/embeddings&lt;/code> y &lt;code>/rerank&lt;/code>), &lt;strong>fastembed&lt;/strong> de Qdrant (ONNX-CPU), &lt;code>bge-m3&lt;/code> en ONNX int8 con sus tres cabezas (dense/sparse/ColBERT). El blog de Intel + Hugging Face con Optimum Intel y fastRAG reporta &lt;strong>hasta ~10× en indexación&lt;/strong> para BGE-large int8 sobre un Xeon de 4ª gen (cifra de su benchmark, encoding-only; la cito y la matizo abajo). La conclusión operativa: &lt;strong>separa el plano de datos (CPU) del plano de generación (GPU)&lt;/strong>. En el cluster 4×H100, ninguna H100 debería gastarse en re-indexar un corpus que cambia una vez al día —eso va a la flota CPU genérica (Xeon AMX, NUCs)— y las H100 se reservan para generar y, como mucho, para picos de rerank o embedders grandes de 7B. Lo que &lt;strong>no&lt;/strong> baja a CPU: generación interactiva, reranking masivo a alto QPS, re-indexación con SLA estricto en tiempo real y embedders de 7B (gte-Qwen2, NV-Embed).&lt;/p>
&lt;h2 id="la-analogía-la-biblioteca-y-el-bibliotecario">La analogía: la biblioteca y el bibliotecario&lt;/h2>
&lt;p>Imagina una biblioteca de investigación seria. Hay tres trabajos distintos, hechos por personas distintas, con relojes distintos.&lt;/p>
&lt;p>El primero es la &lt;strong>catalogación&lt;/strong>. Llegan cajas de libros nuevos; alguien los abre, los clasifica, les asigna signatura, los indexa en el catálogo y los coloca en la estantería correcta. Es trabajo paciente, de fondo, que se hace &lt;strong>de noche o entre horas&lt;/strong>. Nadie está esperando con un cronómetro a que termines de catalogar el lote de hoy: lo que importa es que mañana esté hecho y bien hecho. Es &lt;strong>throughput puro&lt;/strong>: cuántos libros catalogas por hora, no cuánto tardas en catalogar uno concreto. Esto es la &lt;strong>ingesta&lt;/strong>.&lt;/p>
&lt;p>El segundo es &lt;strong>atender una consulta en el mostrador&lt;/strong>. Un lector llega y pregunta por un tema. El bibliotecario va al catálogo —que ya está construido—, localiza media docena de signaturas relevantes, las va a buscar a la estantería y le pone los libros encima del mostrador. Es rápido, ligero, y consiste en &lt;strong>buscar en un índice que ya existe&lt;/strong>, no en construirlo. Esto es el &lt;strong>retrieval&lt;/strong>.&lt;/p>
&lt;p>El tercero es &lt;strong>redactar un informe razonado&lt;/strong> a partir de esos libros. El lector —o un experto al que se lo encargas— lee los seis libros, los compara, sintetiza, escribe una respuesta argumentada con citas. Esto es lento, exige una cabeza muy entrenada, y &lt;strong>el lector está esperando&lt;/strong>: aquí sí hay un cronómetro humano. Esto es la &lt;strong>generación&lt;/strong>, el LLM.&lt;/p>
&lt;p>La moraleja es la del reparto del personal. &lt;strong>No pones a tu redactor estrella —caro, escaso, con cola de gente esperando sus informes— a catalogar cajas de libros de madrugada.&lt;/strong> Catalogar lo hace un equipo numeroso y barato que trabaja por la noche sin prisa. El redactor estrella solo toca lo que de verdad necesita su cabeza: redactar. En nuestro sistema, el redactor estrella es la H100, y catalogar de madrugada es la ingesta del corpus. Gastar la H100 re-indexando es exactamente el error de poner al redactor a etiquetar cajas.&lt;/p>
&lt;p>El resto del post es, esencialmente, qué partes del trabajo de biblioteca puede hacer el equipo barato de la CPU (casi todas) y cuál es irrenunciablemente del redactor en GPU (solo la última).&lt;/p>
&lt;h2 id="las-tres-fases-y-sus-perfiles-de-cómputo">Las tres fases y sus perfiles de cómputo&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Las tres fases del RAG: construcción y retrieval en CPU, generación en GPU">
&lt;text x="410" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Las tres fases del RAG y dónde corre cada una&lt;/text>
&lt;rect x="20" y="50" width="250" height="190" fill="none" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="145" y="74" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">1 · CONSTRUCCIÓN / INGESTA&lt;/text>
&lt;text x="145" y="94" text-anchor="middle" font-size="11" fill="currentColor">chunking del corpus&lt;/text>
&lt;text x="145" y="111" text-anchor="middle" font-size="11" fill="currentColor">embedding dense (bge-m3)&lt;/text>
&lt;text x="145" y="128" text-anchor="middle" font-size="11" fill="currentColor">cabeza sparse / SPLADE&lt;/text>
&lt;text x="145" y="145" text-anchor="middle" font-size="11" fill="currentColor">construir índice HNSW&lt;/text>
&lt;rect x="45" y="162" width="200" height="26" fill="#22c55e" stroke="none"/>
&lt;text x="145" y="180" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">[CPU]&lt;/text>
&lt;text x="145" y="208" text-anchor="middle" font-size="11" font-style="italic" fill="currentColor">throughput-bound · batch&lt;/text>
&lt;text x="145" y="224" text-anchor="middle" font-size="11" font-style="italic" fill="currentColor">sin SLA de latencia · nocturno&lt;/text>
&lt;rect x="285" y="50" width="250" height="190" fill="none" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="410" y="74" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">2 · RETRIEVAL (query-time)&lt;/text>
&lt;text x="410" y="94" text-anchor="middle" font-size="11" fill="currentColor">embedding de la query&lt;/text>
&lt;text x="410" y="111" text-anchor="middle" font-size="11" fill="currentColor">búsqueda HNSW (dense)&lt;/text>
&lt;text x="410" y="128" text-anchor="middle" font-size="11" fill="currentColor">búsqueda sparse + RRF&lt;/text>
&lt;text x="410" y="145" text-anchor="middle" font-size="11" fill="currentColor">rerank top-20/50 (ligero)&lt;/text>
&lt;rect x="310" y="162" width="200" height="26" fill="#3b82f6" stroke="none"/>
&lt;text x="410" y="180" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">[CPU] (rerank: matiz)&lt;/text>
&lt;text x="410" y="208" text-anchor="middle" font-size="11" font-style="italic" fill="currentColor">latencia baja pero tolerable&lt;/text>
&lt;text x="410" y="224" text-anchor="middle" font-size="11" font-style="italic" fill="currentColor">decenas de ms · HNSW siempre CPU&lt;/text>
&lt;rect x="550" y="50" width="250" height="190" fill="none" stroke="#ef4444" stroke-width="1.6"/>
&lt;text x="675" y="74" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">3 · GENERACIÓN&lt;/text>
&lt;text x="675" y="98" text-anchor="middle" font-size="11" fill="currentColor">el LLM produce la respuesta&lt;/text>
&lt;text x="675" y="115" text-anchor="middle" font-size="11" fill="currentColor">prefill del contexto aumentado&lt;/text>
&lt;text x="675" y="132" text-anchor="middle" font-size="11" fill="currentColor">decode token a token&lt;/text>
&lt;rect x="575" y="162" width="200" height="26" fill="#ef4444" stroke="none"/>
&lt;text x="675" y="180" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">[GPU]&lt;/text>
&lt;text x="675" y="208" text-anchor="middle" font-size="11" font-style="italic" fill="currentColor">latency-bound · TTFT importa&lt;/text>
&lt;text x="675" y="224" text-anchor="middle" font-size="11" font-style="italic" fill="currentColor">7B en CPU = TTFT de segundos&lt;/text>
&lt;path d="M270,145 L285,145" stroke="currentColor" stroke-width="1.6" fill="none"/>
&lt;path d="M535,145 L550,145" stroke="currentColor" stroke-width="1.6" fill="none"/>
&lt;line x1="20" y1="270" x2="800" y2="270" stroke="currentColor" stroke-width="1"/>
&lt;text x="20" y="290" font-size="12" font-weight="700" fill="currentColor">PLANO DE DATOS (CPU)&lt;/text>
&lt;text x="520" y="290" font-size="12" font-weight="700" fill="currentColor">PLANO DE GENERACIÓN (GPU)&lt;/text>
&lt;rect x="20" y="300" width="510" height="20" fill="#22c55e" opacity="0.25" stroke="#22c55e" stroke-width="1"/>
&lt;rect x="550" y="300" width="250" height="20" fill="#ef4444" opacity="0.25" stroke="#ef4444" stroke-width="1"/>
&lt;text x="275" y="314" text-anchor="middle" font-size="11" fill="currentColor">fases 1 y 2 · flota CPU genérica · barato y horizontal&lt;/text>
&lt;text x="675" y="314" text-anchor="middle" font-size="11" fill="currentColor">fase 3 · GPU escasa&lt;/text>
&lt;text x="410" y="345" text-anchor="middle" font-size="12" font-style="italic" fill="currentColor">La frontera del sistema cae entre retrieval y generación, no entre "IA" y "no-IA"&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La confusión de la que vive el sobre-aprovisionamiento de GPU es tratar &amp;ldquo;el RAG&amp;rdquo; como un bloque monolítico que &amp;ldquo;usa IA, luego va a la GPU&amp;rdquo;. No. El RAG es un &lt;strong>pipeline de datos con un modelo generativo enchufado al final&lt;/strong>. La frontera arquitectónica correcta no separa &amp;ldquo;lo que usa modelos&amp;rdquo; de &amp;ldquo;lo que no&amp;rdquo; —ambos lados usan modelos—, sino &lt;strong>throughput-bound de latency-bound&lt;/strong>, que es lo mismo que separar el &lt;strong>plano de datos&lt;/strong> del &lt;strong>plano de generación&lt;/strong>.&lt;/p>
&lt;h2 id="por-qué-la-ingesta-encaja-en-cpu-el-embedder-no-es-un-llm">Por qué la ingesta encaja en CPU: el embedder no es un LLM&lt;/h2>
&lt;p>El argumento entero descansa en una asimetría de tamaño que se pasa por alto. La gente oye &amp;ldquo;embeddings&amp;rdquo; y &amp;ldquo;generación&amp;rdquo; y los mete en el mismo saco de &amp;ldquo;modelos grandes que necesitan GPU&amp;rdquo;. Pero el encoder de embeddings y el LLM generativo están &lt;strong>dos órdenes de magnitud de distancia&lt;/strong> en parámetros.&lt;/p>
&lt;p>&lt;code>bge-m3&lt;/code> —el embedder multilingüe de referencia— es un &lt;strong>XLM-RoBERTa de ~568M parámetros&lt;/strong> (&lt;a href="https://huggingface.co/BAAI/bge-m3">model card&lt;/a>, &lt;a href="https://arxiv.org/abs/2402.03216">paper arXiv:2402.03216&lt;/a>). Su hermano el reranker, &lt;code>bge-reranker-v2-m3&lt;/code>, está construido sobre la misma base y ronda los mismos &lt;strong>~568M parámetros&lt;/strong> (&lt;a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">model card&lt;/a>). Compáralo con un LLM generativo de gama de entrada: un Llama 3.1 &lt;strong>8B&lt;/strong> tiene ~14× más parámetros, y los grandes de producción andan por 70B+. Un encoder de 568M es, en presupuesto de cómputo, &lt;strong>otro animal&lt;/strong>.&lt;/p>
&lt;p>Dos diferencias estructurales hacen que ese encoder sea cómodo en CPU:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Es un encoder, no un decoder autoregresivo.&lt;/strong> Procesa la secuencia entera en &lt;strong>un único forward pass&lt;/strong> y emite el vector. No hay decode token a token, no hay KV cache que crece, no hay la fase de generación memory-bound que mata a la CPU. Es un pase denso de matrices y se acabó.&lt;/li>
&lt;li>&lt;strong>Cuantiza a int8 sin apenas pérdida.&lt;/strong> En int8, &lt;code>bge-m3&lt;/code> ocupa del orden de &lt;strong>~580 MB&lt;/strong> y, sobre todo, activa las &lt;strong>rutas de cómputo entero&lt;/strong> que la CPU moderna ejecuta deprisa: instrucciones matriciales tipo &lt;strong>Intel AMX&lt;/strong> (Advanced Matrix Extensions, Xeon de 4ª generación en adelante), &lt;strong>AVX-512 con VNNI&lt;/strong> (Vector Neural Network Instructions) en Xeon previos, y &lt;strong>NEON SDOT/UDOT&lt;/strong> en ARM. La pérdida de calidad de pasar FP32 a int8 en estos modelos suele quedar &lt;strong>por debajo del 1%&lt;/strong> de recall de recuperación, prácticamente invisible (&lt;a href="https://huggingface.co/blog/intel-fast-embedding">Intel + Hugging Face, &lt;em>CPU Optimized Embeddings&lt;/em>&lt;/a>).&lt;/li>
&lt;/ul>
&lt;p>Cuantifiquemos el tamaño del int8. Para $P = 568 \times 10^6$ parámetros a 1 byte cada uno:&lt;/p>
&lt;p>$$\text{tamaño}_{\text{int8}} \approx 568 \times 10^6 \text{ params} \times 1 \text{ byte/param} \approx 568 \text{ MB}$$&lt;/p>
&lt;p>Es decir, el modelo cabe en la caché y la RAM de cualquier servidor o NUC sin pestañear, y el cuello de botella es de &lt;strong>cómputo entero&lt;/strong>, justo lo que AMX/VNNI aceleran. No hay nada en este perfil que pida una GPU.&lt;/p>
&lt;h3 id="runtimes-que-ya-hacen-esto-sin-esfuerzo">Runtimes que ya hacen esto sin esfuerzo&lt;/h3>
&lt;p>No hay que inventar nada. El ecosistema CPU para el plano de datos está maduro:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Text Embeddings Inference (TEI)&lt;/strong> de Hugging Face: servidor en Rust con &lt;strong>backends CPU&lt;/strong> vía ONNX Runtime (recomendado) o Intel MKL, y endpoints &lt;strong>OpenAI-compatibles&lt;/strong> (&lt;code>/v1/embeddings&lt;/code>) además de &lt;code>/rerank&lt;/code> (&lt;a href="https://github.com/huggingface/text-embeddings-inference">repo TEI&lt;/a>). Es decir, el plano de datos en CPU expone exactamente el mismo contrato HTTP que un servidor GPU; el resto del sistema no se entera de qué silicio hay detrás.&lt;/li>
&lt;li>&lt;strong>fastembed&lt;/strong> de Qdrant: librería ligera que carga embedders en &lt;strong>ONNX-CPU&lt;/strong> y genera vectores dense, sparse y ColBERT (&lt;a href="https://github.com/qdrant/fastembed">repo fastembed&lt;/a>). Pensada de origen para correr sin GPU.&lt;/li>
&lt;li>&lt;strong>&lt;code>bge-m3&lt;/code> en ONNX int8&lt;/strong> con sus &lt;strong>tres cabezas&lt;/strong> (dense / sparse-lexical / ColBERT multivector) exportadas y cuantizadas, listas para ONNX Runtime CPU.&lt;/li>
&lt;/ul>
&lt;p>El dato de Intel y Hugging Face que ancla la viabilidad: en su benchmark con &lt;strong>Optimum Intel + fastRAG&lt;/strong> sobre un &lt;strong>Xeon de 4ª generación (8480+, 56 cores, 1 socket)&lt;/strong>, la variante int8 de &lt;strong>BGE-large&lt;/strong> alcanza &lt;strong>hasta ~10× de throughput de indexación&lt;/strong> frente a FP32 (&lt;a href="https://huggingface.co/blog/intel-fast-embedding">HF blog&lt;/a>, &lt;a href="https://haystack.deepset.ai/blog/cpu-optimized-models-with-fastrag">Haystack/deepset&lt;/a>). Hay que leer la letra pequeña y la leo: ese ~10× es &lt;strong>encoding-only&lt;/strong> (tokenización excluida), a secuencia 256, comparando int8 contra FP32 &lt;strong>en la misma CPU&lt;/strong> —no es &amp;ldquo;CPU 10× más rápido que GPU&amp;rdquo;, es &amp;ldquo;int8 10× más rápido que FP32 en CPU&amp;rdquo;—. Sigue siendo el dato relevante: te dice que con cuantización la CPU pasa de inviable a perfectamente útil para ingesta batch.&lt;/p>
&lt;h2 id="tabla-de-viabilidad-cpu-para-cada-componente">Tabla de viabilidad: ¿CPU para cada componente?&lt;/h2>
&lt;p>Esta es la tabla operativa. La columna que importa es la del &lt;strong>matiz&lt;/strong>, porque &amp;ldquo;sí&amp;rdquo; y &amp;ldquo;no&amp;rdquo; a secas mienten.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Componente&lt;/th>
&lt;th>¿CPU viable?&lt;/th>
&lt;th>Matiz&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Chunking&lt;/strong> (trocear el corpus)&lt;/td>
&lt;td>&lt;strong>Sí, siempre&lt;/strong>&lt;/td>
&lt;td>Es regex, parsing y ventanas; nunca tuvo nada que ver con GPU.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Embedding ingesta&lt;/strong> &lt;code>bge-m3&lt;/code> dense&lt;/td>
&lt;td>&lt;strong>Sí, su mejor caso&lt;/strong>&lt;/td>
&lt;td>Batch nocturno, int8 + AMX/VNNI. Es exactamente para lo que la CPU brilla.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cabeza sparse / SPLADE / BM25&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Sí, nativo CPU&lt;/strong>&lt;/td>
&lt;td>El léxico es inverted-index puro; la GPU no aporta nada aquí.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Construir índice HNSW&lt;/strong> (Qdrant, pgvector)&lt;/td>
&lt;td>&lt;strong>Sí, siempre CPU&lt;/strong>&lt;/td>
&lt;td>El build del grafo HNSW es CPU por diseño en estos motores.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Embedding de query&lt;/strong> (online)&lt;/td>
&lt;td>&lt;strong>Sí&lt;/strong>&lt;/td>
&lt;td>Un solo texto corto; decenas de ms en CPU, sobra para chat.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Búsqueda dense + sparse + RRF&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Sí&lt;/strong>&lt;/td>
&lt;td>La búsqueda vectorial &lt;strong>siempre&lt;/strong> fue CPU, incluso en stacks &amp;ldquo;GPU&amp;rdquo;. RRF es ordenar listas.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Reranker cross-encoder&lt;/strong> &lt;code>bge-reranker-v2-m3&lt;/code> top-20/50&lt;/td>
&lt;td>&lt;strong>Sí, con cuidado&lt;/strong>&lt;/td>
&lt;td>Un cross-encoder evalúa $k$ pares query-doc: coste $\propto k$. Sobre 20-50 candidatos va; sobre cientos a alto QPS, no.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ColBERT late-interaction&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Marginal en CPU&lt;/strong>&lt;/td>
&lt;td>El producto de matrices token-a-token de la interacción tardía es pesado; viable en volúmenes bajos, sufre con QPS.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Generación LLM&lt;/strong>&lt;/td>
&lt;td>&lt;strong>No, en la práctica&lt;/strong>&lt;/td>
&lt;td>Un 7B en CPU da TTFT de segundos. Latencia interactiva = GPU.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dos filas merecen subrayado porque desmontan mitos.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;La búsqueda vectorial necesita GPU.&amp;rdquo;&lt;/strong> Falso de origen. El índice &lt;strong>HNSW&lt;/strong> —el grafo navegable de pequeño mundo que usan Qdrant, pgvector con &lt;code>vector&lt;/code>/&lt;code>halfvec&lt;/code>, Milvus en su modo CPU y casi todo lo demás— &lt;strong>siempre se construyó y se recorrió en CPU&lt;/strong>. Incluso los stacks que se anuncian como &amp;ldquo;GPU-accelerated RAG&amp;rdquo; hacen el embedding en GPU pero la &lt;strong>búsqueda ANN sigue en CPU&lt;/strong> en la inmensa mayoría de despliegues; las variantes GPU del índice (CAGRA y similares) son la excepción cara, no la norma, y se justifican solo con miles de millones de vectores y QPS extremo. Para un corpus corporativo de millones de chunks, HNSW en CPU resuelve en &lt;strong>single-digit milisegundos&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;El reranker es un modelo, luego GPU.&amp;rdquo;&lt;/strong> El reranker &lt;code>bge-reranker-v2-m3&lt;/code> es un cross-encoder de ~568M: corre en CPU. El matiz es el &lt;strong>número de pares&lt;/strong>. Un cross-encoder no produce un vector reutilizable; evalúa la pareja (query, documento) junta, así que su coste crece &lt;strong>linealmente con los candidatos&lt;/strong> $k$:&lt;/p>
&lt;p>$$\text{coste}_{\text{rerank}} \propto k \times \text{forward}(\text{query} + \text{doc})$$&lt;/p>
&lt;p>Rerankear el &lt;strong>top-20 o top-50&lt;/strong> que sale del retrieval híbrido es perfectamente asumible en CPU. Rerankear &lt;strong>cientos&lt;/strong> de candidatos a &lt;strong>alto QPS&lt;/strong> no: ahí el coste lineal se dispara y la GPU gana. La regla práctica: &lt;strong>recall amplio barato en el retriever, rerank de precisión sobre pocos candidatos&lt;/strong>. (El detalle de hybrid retrieval y reranking está en la pieza de fundamentos enlazada abajo.)&lt;/p>
&lt;h2 id="los-números-con-metodología-honesta">Los números, con metodología honesta&lt;/h2>
&lt;p>Aquí viene la parte donde mucha gente miente por omisión. Voy a dar rangos de throughput, pero &lt;strong>son rangos de literatura y de orden de magnitud, no medidas mías en este hardware&lt;/strong>. Tómalos como tales: la decisión correcta no depende de clavar el número, depende de entender el reparto.&lt;/p>
&lt;p>Para &lt;code>bge-m3&lt;/code> dense, secuencia ≈256 tokens, el throughput de embedding se mueve aproximadamente así:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Plataforma&lt;/th>
&lt;th>Throughput dense (orden de magnitud)&lt;/th>
&lt;th>Lectura&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>GPU gama alta&lt;/strong> (5090 fp16, TEI)&lt;/td>
&lt;td>~12k tok/s+ (orientativo)&lt;/td>
&lt;td>El techo; caro y escaso.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CPU servidor grande&lt;/strong> (Xeon ~56 cores, int8 ONNX)&lt;/td>
&lt;td>banda baja de &lt;strong>miles&lt;/strong> tok/s&lt;/td>
&lt;td>~1/5–1/10 de la GPU, pero escalable horizontal y barato.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CPU edge / NUC&lt;/strong> (4-8 cores, int8)&lt;/td>
&lt;td>&lt;strong>decenas a bajos cientos&lt;/strong> tok/s&lt;/td>
&lt;td>Suficiente para ingesta nocturna de un corpus local.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La tentación es leer la segunda fila como &amp;ldquo;CPU es 5-10× más lento, descartado&amp;rdquo;. &lt;strong>Es la lectura equivocada para la ingesta.&lt;/strong> Para trabajo batch sin SLA, lo que mandan no son los tok/s absolutos sino el &lt;strong>throughput por euro&lt;/strong> y el &lt;strong>throughput por vatio&lt;/strong> —y ahí la cuenta cambia de signo.&lt;/p>
&lt;p>Pongamos un ejemplo numérico de reparto. Supón un corpus de &lt;strong>2 millones de chunks&lt;/strong> de ~256 tokens que hay que re-indexar &lt;strong>una vez al día&lt;/strong> (cambia el corpus, hay que rehacer embeddings). Eso son:&lt;/p>
&lt;p>$$2 \times 10^6 \text{ chunks} \times 256 \text{ tok/chunk} \approx 5.1 \times 10^8 \text{ tokens}$$&lt;/p>
&lt;p>A un throughput CPU conservador de, digamos, &lt;strong>3000 tok/s&lt;/strong> por servidor Xeon int8:&lt;/p>
&lt;p>$$t_{\text{ingesta}} \approx \frac{5.1 \times 10^8 \text{ tok}}{3000 \text{ tok/s}} \approx 1.7 \times 10^5 \text{ s} \approx 47 \text{ horas en un solo servidor}$$&lt;/p>
&lt;p>47 horas en &lt;strong>una&lt;/strong> caja suena mal hasta que recuerdas dos cosas. Primero, esto es &lt;strong>vergonzosamente paralelo&lt;/strong>: el corpus se trocea y se reparte; con &lt;strong>8 servidores CPU&lt;/strong> baja a ~6 horas, con 16 a ~3 horas, holgadamente dentro de la ventana nocturna. Segundo, y más importante: &lt;strong>ese mismo trabajo en la GPU bloquea la GPU&lt;/strong>. Si la H100 hace 12k tok/s, tarda ~12 horas&amp;hellip; pero son 12 horas de &lt;strong>la H100&lt;/strong>, el recurso por el que se pelea toda la organización para &lt;strong>generar&lt;/strong>. Gastar el recurso escaso y caro en re-indexar un corpus que cambia una vez al día es un &lt;strong>mal reparto&lt;/strong>, aunque sea &amp;ldquo;más rápido&amp;rdquo;: estás optimizando el tok/s equivocado.&lt;/p>
&lt;p>La regla mental: &lt;strong>para la ingesta, optimiza throughput/€ y throughput/W; los tok/s absolutos son del plano de generación, donde el cronómetro humano sí corre.&lt;/strong>&lt;/p>
&lt;h2 id="árbol-de-decisión-cpu-o-gpu-para-esta-pieza">Árbol de decisión: ¿CPU o GPU para esta pieza?&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Árbol de decisión CPU vs GPU por componente del RAG">
&lt;defs>&lt;marker id="dt" 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="currentColor"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="24" text-anchor="middle" font-size="14" font-weight="700" fill="currentColor">¿Esta pieza del RAG va a CPU o a GPU?&lt;/text>
&lt;rect x="270" y="44" width="240" height="40" rx="6" fill="#f59e0b" opacity="0.18" stroke="#f59e0b" stroke-width="1.4"/>
&lt;text x="390" y="68" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">¿Está el usuario esperando (SLA interactivo)?&lt;/text>
&lt;path d="M390,84 L200,120" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#dt)"/>
&lt;text x="270" y="104" text-anchor="middle" font-size="11" fill="currentColor">no (batch)&lt;/text>
&lt;path d="M390,84 L580,120" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#dt)"/>
&lt;text x="510" y="104" text-anchor="middle" font-size="11" fill="currentColor">sí (online)&lt;/text>
&lt;rect x="60" y="124" width="280" height="38" rx="6" fill="#22c55e" opacity="0.15" stroke="#22c55e" stroke-width="1.4"/>
&lt;text x="200" y="148" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Ingesta/indexado → [CPU] flota barata&lt;/text>
&lt;rect x="440" y="124" width="290" height="40" rx="6" fill="#f59e0b" opacity="0.18" stroke="#f59e0b" stroke-width="1.4"/>
&lt;text x="585" y="142" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">¿Genera tokens (es el LLM)?&lt;/text>
&lt;text x="585" y="157" text-anchor="middle" font-size="11" fill="currentColor">¿o es retrieval/rerank?&lt;/text>
&lt;path d="M585,164 L460,210" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#dt)"/>
&lt;text x="500" y="190" text-anchor="middle" font-size="11" fill="currentColor">retrieval&lt;/text>
&lt;path d="M585,164 L690,210" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#dt)"/>
&lt;text x="680" y="190" text-anchor="middle" font-size="11" fill="currentColor">genera&lt;/text>
&lt;rect x="330" y="214" width="250" height="40" rx="6" fill="#f59e0b" opacity="0.18" stroke="#f59e0b" stroke-width="1.4"/>
&lt;text x="455" y="232" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">embed query / HNSW / RRF / rerank&lt;/text>
&lt;text x="455" y="248" text-anchor="middle" font-size="11" fill="currentColor">¿cuántos candidatos y a qué QPS?&lt;/text>
&lt;rect x="600" y="214" width="150" height="38" rx="6" fill="#ef4444" opacity="0.18" stroke="#ef4444" stroke-width="1.4"/>
&lt;text x="675" y="238" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">[GPU] generación&lt;/text>
&lt;path d="M455,254 L250,300" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#dt)"/>
&lt;text x="320" y="280" text-anchor="middle" font-size="11" fill="currentColor">embed/HNSW/RRF, o rerank top-20/50&lt;/text>
&lt;path d="M455,254 L600,300" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#dt)"/>
&lt;text x="600" y="280" text-anchor="middle" font-size="11" fill="currentColor">rerank cientos&lt;/text>
&lt;text x="600" y="293" text-anchor="middle" font-size="11" fill="currentColor">alto QPS / ColBERT&lt;/text>
&lt;rect x="110" y="304" width="280" height="38" rx="6" fill="#22c55e" opacity="0.15" stroke="#22c55e" stroke-width="1.4"/>
&lt;text x="250" y="328" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">[CPU] plano de datos&lt;/text>
&lt;rect x="510" y="304" width="200" height="38" rx="6" fill="#ef4444" opacity="0.18" stroke="#ef4444" stroke-width="1.4"/>
&lt;text x="610" y="322" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">[GPU] rerank pesado&lt;/text>
&lt;text x="610" y="336" text-anchor="middle" font-size="10" fill="currentColor">(o embedder 7B grande)&lt;/text>
&lt;text x="390" y="372" text-anchor="middle" font-size="12" font-style="italic" fill="currentColor">Regla: solo cruza a GPU lo latency-bound (generación) y lo masivo-online (rerank a alto QPS).&lt;/text>
&lt;text x="390" y="392" text-anchor="middle" font-size="12" font-style="italic" fill="currentColor">Todo lo demás —que es casi todo— se queda en el plano de datos en CPU.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="arquitectura-de-referencia-a-cpu-only">Arquitectura de referencia (a): CPU-only&lt;/h2>
&lt;p>El primer caso es un &lt;strong>nodo sin GPU&lt;/strong>: un NUC, un Xeon de oficina, un servidor edge soberano en una sucursal o en un entorno aislado. Todo el plano de datos vive ahí; la generación se &lt;strong>delega&lt;/strong> a un endpoint GPU remoto o se hace en batch con un SLM cuando la latencia no apremia.&lt;/p>
&lt;p>El stack:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TEI-CPU&lt;/strong> sirviendo &lt;code>bge-m3&lt;/code> int8 con dense + sparse (mismo contrato OpenAI &lt;code>/v1/embeddings&lt;/code>, más &lt;code>/rerank&lt;/code> para el reranker).&lt;/li>
&lt;li>&lt;strong>Qdrant&lt;/strong> con índice &lt;strong>HNSW&lt;/strong> dense + vectores &lt;strong>sparse&lt;/strong>, fusión &lt;strong>RRF&lt;/strong> nativa.&lt;/li>
&lt;li>&lt;strong>Reranker&lt;/strong> &lt;code>bge-reranker-v2-m3&lt;/code> sobre el top-k (vía el &lt;code>/rerank&lt;/code> de TEI).&lt;/li>
&lt;li>&lt;strong>Gateway&lt;/strong> que orquesta y, para generar, llama a un endpoint externo.&lt;/li>
&lt;/ul>
&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="c"># docker-compose: plano de datos RAG completo en CPU (sin GPU)&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">services&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">tei-embed&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/huggingface/text-embeddings-inference:cpu-latest&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">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--model-id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;BAAI/bge-m3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--pooling&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;cls&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--dtype&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;int8&amp;#34;&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">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;8081:80&amp;#34;&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="c"># backend ONNX/MKL: aprovecha AVX-512+VNNI / AMX si el Xeon lo soporta&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="nt">tei-rerank&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/huggingface/text-embeddings-inference:cpu-latest&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">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--model-id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;BAAI/bge-reranker-v2-m3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--dtype&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;int8&amp;#34;&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">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;8082:80&amp;#34;&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="c"># expone /rerank — se invoca SOLO sobre top-20/50, no sobre cientos&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="nt">qdrant&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qdrant/qdrant:latest&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">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;6333:6333&amp;#34;&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">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;./qdrant_storage:/qdrant/storage&amp;#34;&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="c"># HNSW dense + sparse vectors + RRF, todo CPU&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Búsqueda híbrida con fusión RRF en Qdrant (dense + sparse en una sola query):&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="kn">from&lt;/span> &lt;span class="nn">qdrant_client&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">QdrantClient&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">models&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">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">QdrantClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://qdrant:6333&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"># embed de la query: dense y sparse desde el TEI-CPU (omitido el wiring HTTP)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">hits&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">query_points&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">collection_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;corpus&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">prefetch&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Prefetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">query&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">dense_vec&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">using&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dense&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Prefetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">query&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">sparse_vec&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">using&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sparse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">50&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 class="n">query&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">FusionQuery&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fusion&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Fusion&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RRF&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># RRF nativo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">20&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 class="o">.&lt;/span>&lt;span class="n">points&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -&amp;gt; luego: POST /rerank (TEI) sobre estos 20, te quedas con top-5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -&amp;gt; luego: el gateway manda query + top-5 al endpoint de GENERACIÓN (GPU)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La generación, en este nodo CPU-only, &lt;strong>sale del nodo&lt;/strong>: el gateway construye el prompt aumentado y lo envía a un endpoint vLLM en el cluster GPU (o, si no hay SLA interactivo, a un SLM en CPU en modo batch, asumiendo TTFT de segundos). El plano de datos entero —lo de arriba— corre sin una sola GPU.&lt;/p>
&lt;h2 id="arquitectura-de-referencia-b-híbrida-recomendada">Arquitectura de referencia (b): híbrida recomendada&lt;/h2>
&lt;p>Esta es la que recomiendo para el caso general con cluster GPU disponible: &lt;strong>plano de datos en CPU, plano de generación en GPU&lt;/strong>, comunicados por contratos HTTP OpenAI-compatibles para que cada lado sea sustituible.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Arquitectura híbrida: plano de datos CPU + plano de generación GPU">
&lt;defs>&lt;marker id="hy" 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="currentColor"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="24" text-anchor="middle" font-size="14" font-weight="700" fill="currentColor">Arquitectura híbrida: dos planos, dos silicios&lt;/text>
&lt;rect x="30" y="50" width="470" height="210" rx="8" fill="#22c55e" opacity="0.08" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="265" y="72" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">PLANO DE DATOS — flota CPU (Xeon AMX / NUC)&lt;/text>
&lt;rect x="50" y="88" width="130" height="50" rx="5" fill="none" stroke="#3b82f6" stroke-width="1.3"/>
&lt;text x="115" y="108" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">Ingesta batch&lt;/text>
&lt;text x="115" y="124" text-anchor="middle" font-size="10" fill="currentColor">chunk + embed int8&lt;/text>
&lt;rect x="200" y="88" width="130" height="50" rx="5" fill="none" stroke="#3b82f6" stroke-width="1.3"/>
&lt;text x="265" y="108" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">Qdrant&lt;/text>
&lt;text x="265" y="124" text-anchor="middle" font-size="10" fill="currentColor">HNSW+sparse+RRF&lt;/text>
&lt;rect x="350" y="88" width="130" height="50" rx="5" fill="none" stroke="#3b82f6" stroke-width="1.3"/>
&lt;text x="415" y="108" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">TEI rerank&lt;/text>
&lt;text x="415" y="124" text-anchor="middle" font-size="10" fill="currentColor">top-20/50 ligero&lt;/text>
&lt;rect x="50" y="160" width="430" height="44" rx="5" fill="none" stroke="#22c55e" stroke-width="1.3"/>
&lt;text x="265" y="180" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">TEI-CPU /v1/embeddings + /rerank · int8 · AVX-512/AMX&lt;/text>
&lt;text x="265" y="196" text-anchor="middle" font-size="10" fill="currentColor">contrato OpenAI-compatible: la GPU no sabe que esto es CPU&lt;/text>
&lt;rect x="540" y="50" width="250" height="210" rx="8" fill="#ef4444" opacity="0.08" stroke="#ef4444" stroke-width="1.6"/>
&lt;text x="665" y="72" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">PLANO DE GENERACIÓN — GPU&lt;/text>
&lt;rect x="560" y="100" width="210" height="60" rx="5" fill="none" stroke="#ef4444" stroke-width="1.3"/>
&lt;text x="665" y="124" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">vLLM · LLM 7B+&lt;/text>
&lt;text x="665" y="142" text-anchor="middle" font-size="10" fill="currentColor">prefill + decode interactivo&lt;/text>
&lt;rect x="560" y="172" width="210" height="44" rx="5" fill="none" stroke="#ef4444" stroke-width="1.3"/>
&lt;text x="665" y="192" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">opcional: rerank pesado / embedder 7B&lt;/text>
&lt;text x="665" y="207" text-anchor="middle" font-size="10" fill="currentColor">solo picos que la CPU no absorbe&lt;/text>
&lt;path d="M480,182 L540,150" stroke="currentColor" stroke-width="1.8" fill="none" marker-end="url(#hy)"/>
&lt;text x="510" y="155" text-anchor="middle" font-size="10" fill="currentColor">query+top-k&lt;/text>
&lt;text x="410" y="284" text-anchor="middle" font-size="12" font-style="italic" fill="currentColor">El gateway habla OpenAI por HTTP con ambos lados; cada plano escala por separado.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Servidor de generación, mínimo, en GPU:&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="c"># vLLM en el cluster GPU — SOLO generación&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">services&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">vllm-gen&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:latest&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">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --model meta-llama/Llama-3.1-8B-Instruct
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --dtype bfloat16 --max-model-len 8192
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --gpu-memory-utilization 0.85&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="c"># expone /v1/chat/completions — el gateway le manda query + top-5 ya recuperados&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">deploy&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">resources&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">reservations&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">devices&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="nt">driver: nvidia, count: 1, capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">gpu]}]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La virtud del diseño: como &lt;strong>ambos lados hablan el contrato OpenAI por HTTP&lt;/strong>, el plano de datos en CPU y el de generación en GPU &lt;strong>escalan por separado&lt;/strong> y son sustituibles. Si mañana quieres mover el rerank a GPU porque el QPS subió, cambias una URL. Si quieres meter más nodos CPU de ingesta, los añades sin tocar la generación. Todo el stack es &lt;strong>OSS y license-clean&lt;/strong>: &lt;code>bge-m3&lt;/code> y &lt;code>bge-reranker-v2-m3&lt;/code> son &lt;strong>MIT&lt;/strong> (&lt;a href="https://huggingface.co/BAAI/bge-m3">bge-m3&lt;/a>, &lt;a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">reranker&lt;/a>), Qdrant es Apache-2.0, TEI y vLLM son OSS.&lt;/p>
&lt;h2 id="aplicado-al-cluster-genérico-4h100">Aplicado al cluster genérico 4×H100&lt;/h2>
&lt;p>Bajemos esto al cluster de la serie: &lt;strong>4×H100 SXM 80 GB&lt;/strong> más una flota CPU genérica (Xeon con AMX, NUCs). El reparto correcto:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Construcción e indexación → flota CPU.&lt;/strong> Ninguna H100 debería gastar un ciclo re-embebiendo el corpus. Eso va a los Xeon AMX (servidores grandes, throughput de miles de tok/s en int8) o, para corpus locales pequeños, a los NUCs por la noche. El re-indexado nocturno de un corpus que cambia una vez al día es el caso de libro de &amp;ldquo;trabajo de CPU sin prisa&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Las H100 → generación.&lt;/strong> Las cuatro tarjetas se reservan para lo que solo ellas hacen bien: producir tokens a latencia interactiva. Esto es lo que las piezas hermanas de la serie —&lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">compartir GPU&lt;/a> y &lt;a href="https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/">varios modelos en una GPU&lt;/a>— ayudan a exprimir: una vez que la ingesta no compite por la GPU, todo el silicio caro queda libre para generar y se reparte mejor entre modelos y tenants.&lt;/li>
&lt;li>&lt;strong>Las H100, como mucho, → picos de rerank o embedders grandes.&lt;/strong> Si en algún momento necesitas un embedder de &lt;strong>7B&lt;/strong> (gte-Qwen2, NV-Embed) para un dominio donde &lt;code>bge-m3&lt;/code> no llega, o un rerank masivo a QPS que la CPU no absorbe, esos picos sí pueden visitar la GPU. Pero son la &lt;strong>excepción puntual&lt;/strong>, no la carga base.&lt;/li>
&lt;/ul>
&lt;h3 id="el-ángulo-de-auditabilidad-ens--nis2">El ángulo de auditabilidad: ENS / NIS2&lt;/h3>
&lt;p>Hay un argumento de compliance que rara vez se menciona y que el reparto CPU/GPU regala casi gratis.&lt;/p>
&lt;p>Un &lt;strong>nodo CPU-only sin driver propietario&lt;/strong> es &lt;strong>más fácil de auditar&lt;/strong>. No hay stack de kernel cerrado de NVIDIA, no hay versiones de CUDA y firmware que casar con la cadena de suministro, no hay superficie de driver propietario que documentar para un ENS o un NIS2. Todo el plano de datos —chunking, embeddings, índice, búsqueda— corre sobre software OSS en CPU genérica con instrucciones estándar. Para un entorno soberano o clasificado, poder decir &amp;ldquo;el plano que toca el corpus no depende de ningún binario propietario&amp;rdquo; es un argumento real, no marketing.&lt;/p>
&lt;p>Y hay un segundo ángulo de auditabilidad &lt;strong>intrínseco al RAG bien hecho&lt;/strong>: la &lt;strong>trazabilidad de fuentes&lt;/strong>. Un RAG que recupera chunks identificables y los cita es &lt;strong>auditable&lt;/strong> —puedes reconstruir de qué documento salió cada afirmación— frente al &lt;em>context-stuffing&lt;/em> o el conocimiento paramétrico opaco del modelo, donde no hay forma de saber de dónde viene un dato. Esa trazabilidad vive en el &lt;strong>plano de datos&lt;/strong> (qué se recuperó, de qué fuente, con qué score), justo el plano que estamos poniendo en CPU auditable. Los dos argumentos se refuerzan: el silicio auditable y la cadena de evidencia auditable son el mismo plano.&lt;/p>
&lt;h2 id="cuándo-no-llevarlo-a-cpu">Cuándo NO llevarlo a CPU&lt;/h2>
&lt;p>Por honestidad y para no caer en el espejo del hype contrario, los casos donde la CPU &lt;strong>no&lt;/strong> es la respuesta:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Generación a latencia interactiva.&lt;/strong> El caso obvio. Un 7B en CPU da TTFT de &lt;strong>segundos&lt;/strong>: inaceptable para chat. Si el usuario espera, la generación va a GPU. Sin excepciones prácticas a día de hoy.&lt;/li>
&lt;li>&lt;strong>Reranking masivo a alto QPS.&lt;/strong> Un cross-encoder o ColBERT sobre &lt;strong>cientos&lt;/strong> de candidatos, multiplicado por muchas peticiones por segundo, satura la CPU. El coste $\propto k \times \text{QPS}$ cruza el umbral donde la GPU paga. Mantén el rerank CPU acotado a top-20/50; si necesitas más amplitud a más QPS, sube a GPU.&lt;/li>
&lt;li>&lt;strong>Re-indexación en tiempo real con SLA estricto.&lt;/strong> Si el corpus cambia continuamente y la frescura es de segundos (no de horas), el throughput de la CPU puede no alcanzar la ventana. Ahí el embedding de ingesta puede necesitar GPU —pero nota que esto es &lt;strong>raro&lt;/strong>: la mayoría de corpus corporativos cambian a ritmo de horas o días, no de segundos.&lt;/li>
&lt;li>&lt;strong>Embedders grandes (7B).&lt;/strong> &lt;code>bge-m3&lt;/code> (568M) es cómodo en CPU; un &lt;strong>gte-Qwen2&lt;/strong> o &lt;strong>NV-Embed&lt;/strong> de &lt;strong>7B&lt;/strong> vuelve a ser un LLM-class y arrastra el mismo perfil de coste que la generación. Si tu calidad de recuperación exige un embedder de 7B, ese embedder vive donde viven los 7B: en la GPU.&lt;/li>
&lt;/ul>
&lt;p>La frase que resume todo: &lt;strong>la CPU es el sitio por defecto del plano de datos; la GPU es la excepción justificada para lo latency-bound y lo masivo-online.&lt;/strong> Empieza poniendo todo en CPU y sube a GPU solo lo que demuestre que no cabe —no al revés.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/ingesta-documental-rag-pdf-a-chunk-indexado/">Ingesta documental end-to-end: del PDF al chunk indexado&lt;/a> — el pipeline de ingesta que corre en ese plano de datos CPU.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/servir-embeddings-rerankers-tei-produccion/">Servir embeddings y rerankers con TEI en producción&lt;/a> — el motor (TEI) que sirve los embeddings y rerankers de ese plano de datos.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU: time-slicing, MPS y MIG&lt;/a> — la pieza hermana sobre el reparto &lt;em>dentro&lt;/em> de la GPU; una vez que la ingesta sale de la GPU, esto exprime lo que queda.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/embeddings-modelos-2026-dense-sparse-multivector/">Embeddings 2026: dense, sparse y multivector&lt;/a> — las tres cabezas de &lt;code>bge-m3&lt;/code> que el plano de datos sirve en CPU, y cuándo justifica un embedder de 7B que sí pide GPU.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">Reranking e hybrid retrieval: fundamentos&lt;/a> — el detalle de RRF y del rerank cross-encoder cuyo coste lineal decide la frontera CPU/GPU del top-k.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">Ingestión con PostgreSQL y Qdrant en microservicios&lt;/a> — cómo se estructura el pipeline de ingesta que aquí ponemos en la flota CPU.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel: del cluster H100 al NUC&lt;/a> — el hardware concreto de la flota CPU (Xeon AMX, NUC) que sostiene el plano de datos.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/semantic-cache-rag/">Caché semántico para RAG&lt;/a> — otra capa que vive en el plano de datos en CPU y evita tocar la GPU cuando la query ya se respondió.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/rag-agresivo-modelos-pequenos/">RAG agresivo en modelos pequeños&lt;/a> — el lado de generación de esta moneda: cómo el plano de datos curado descarga al modelo de la fase generativa.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Chen, J., et al. &lt;em>BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings&lt;/em>. arXiv 2402.03216. &lt;a href="https://arxiv.org/abs/2402.03216">https://arxiv.org/abs/2402.03216&lt;/a>&lt;/li>
&lt;li>BAAI — &lt;em>BGE-M3 model card&lt;/em> (568M params, XLM-RoBERTa, 8192 tokens, MIT). &lt;a href="https://huggingface.co/BAAI/bge-m3">https://huggingface.co/BAAI/bge-m3&lt;/a>&lt;/li>
&lt;li>BAAI — &lt;em>bge-reranker-v2-m3 model card&lt;/em> (cross-encoder sobre bge-m3, ~568M). &lt;a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">https://huggingface.co/BAAI/bge-reranker-v2-m3&lt;/a>&lt;/li>
&lt;li>Intel + Hugging Face — &lt;em>CPU Optimized Embeddings with Optimum Intel and fastRAG&lt;/em> (~10× indexación BGE-large int8, Xeon 4ª gen). &lt;a href="https://huggingface.co/blog/intel-fast-embedding">https://huggingface.co/blog/intel-fast-embedding&lt;/a>&lt;/li>
&lt;li>deepset / Haystack — &lt;em>CPU-Optimized Embedding Models with fastRAG and Haystack&lt;/em>. &lt;a href="https://haystack.deepset.ai/blog/cpu-optimized-models-with-fastrag">https://haystack.deepset.ai/blog/cpu-optimized-models-with-fastrag&lt;/a>&lt;/li>
&lt;li>Hugging Face — &lt;em>Text Embeddings Inference (TEI)&lt;/em>, backends CPU ONNX/MKL, endpoints OpenAI-compatibles. &lt;a href="https://github.com/huggingface/text-embeddings-inference">https://github.com/huggingface/text-embeddings-inference&lt;/a>&lt;/li>
&lt;li>Qdrant — &lt;em>fastembed&lt;/em> (ONNX-CPU, dense/sparse/ColBERT) y &lt;em>Hybrid Search con RRF&lt;/em>. &lt;a href="https://github.com/qdrant/fastembed">https://github.com/qdrant/fastembed&lt;/a> · &lt;a href="https://qdrant.tech/documentation/beginner-tutorials/hybrid-search-fastembed/">https://qdrant.tech/documentation/beginner-tutorials/hybrid-search-fastembed/&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>