<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Batching on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/batching/</link><description>Recent content in Batching on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 11 Jun 2026 09:40:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/batching/index.xml" rel="self" type="application/rss+xml"/><item><title>Servir embeddings y rerankers con TEI en producción</title><link>https://blog.lo0.es/posts/servir-embeddings-rerankers-tei-produccion/</link><pubDate>Thu, 11 Jun 2026 09:40:00 +0000</pubDate><guid>https://blog.lo0.es/posts/servir-embeddings-rerankers-tei-produccion/</guid><description>&lt;blockquote>
&lt;p>Sexta pieza de una serie operativa sobre exprimir un cluster LLM on-premise genérico de &lt;strong>4×H100 SXM 80 GB&lt;/strong>. Si &lt;a href="https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/">el RAG en CPU&lt;/a> argumentaba &lt;em>dónde&lt;/em> corre el plano de datos y &lt;a href="https://blog.lo0.es/posts/embeddings-modelos-2026-dense-sparse-multivector/">el zoo de embedders&lt;/a> decidía &lt;em>qué modelo&lt;/em> sirves, este post mira &lt;em>el motor&lt;/em> que los sirve: el servidor de embeddings y rerankers. Las hermanas de esta tanda —&lt;a href="https://blog.lo0.es/posts/ingesta-documental-rag-pdf-a-chunk-indexado/">la ingesta documental de PDF a chunk indexado&lt;/a>, &lt;a href="https://blog.lo0.es/posts/gitops-stack-inferencia-llm-flux/">el GitOps del stack de inferencia con Flux&lt;/a> y &lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">el hardening y secretos del stack soberano&lt;/a>— montan el resto del sistema alrededor de esta pieza. El cierre de la serie, el asistente soberano end-to-end con LibreChat + LiteLLM + RAG, sigue en borrador.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Servir embeddings no es &lt;code>model.encode(texto)&lt;/code> esparcido por el código de ingesta. Igual que no metes un &lt;code>transformers.generate()&lt;/code> dentro de tu API y lo llamas vLLM, tampoco metes el embedder inline: lo pones &lt;strong>detrás de un servidor dedicado&lt;/strong> que hace batching, controla la concurrencia, emite métricas y expone &lt;strong>un único contrato HTTP&lt;/strong> que tanto la ingesta como el query-time reutilizan. En el ecosistema open source de 2026 ese servidor es &lt;strong>TEI — Text Embeddings Inference&lt;/strong> de Hugging Face: un binario en &lt;strong>Rust&lt;/strong> que sirve modelos de &lt;strong>embedding&lt;/strong>, &lt;strong>reranker (cross-encoder)&lt;/strong> y &lt;strong>clasificación de secuencias&lt;/strong>, con &lt;strong>batching dinámico por tokens&lt;/strong>, y que expone endpoints &lt;strong>OpenAI-compatibles&lt;/strong> (&lt;code>/v1/embeddings&lt;/code>) además de los nativos &lt;code>/embed&lt;/code>, &lt;code>/embed_sparse&lt;/code>, &lt;code>/rerank&lt;/code> y &lt;code>/predict&lt;/code> (&lt;a href="https://github.com/huggingface/text-embeddings-inference">repo TEI&lt;/a>, &lt;a href="https://huggingface.co/docs/text-embeddings-inference">docs&lt;/a>). Corre en &lt;strong>CPU&lt;/strong> (ONNX Runtime, Intel MKL) y en &lt;strong>CUDA&lt;/strong> (con FlashAttention y cuda graphs), con el mismo contrato a ambos lados, así que el resto del sistema no sabe —ni le importa— qué silicio hay detrás. La pieza técnica que da casi todo el rendimiento es el &lt;strong>dynamic batching&lt;/strong>: TEI agrupa peticiones que llegan por separado en un batch que comparte el coste fijo del forward, controlado por &lt;code>--max-batch-tokens&lt;/code> (cuántos tokens caben por batch) y &lt;code>--max-concurrent-requests&lt;/code> (backpressure). Agrupar &lt;strong>sube el throughput&lt;/strong> —pasar de servir 1 secuencia a 32 multiplica los tokens/s a coste casi constante hasta saturar el hardware— a cambio de &lt;strong>algo de latencia&lt;/strong> mientras se llena la bandeja. Los números: un encoder de &lt;strong>568M&lt;/strong> (&lt;code>bge-m3&lt;/code>, &lt;code>bge-reranker-v2-m3&lt;/code>) ocupa &lt;strong>~1.1 GB en fp16&lt;/strong> y &lt;strong>~0.57 GB en int8&lt;/strong>, así que caben varias réplicas hasta en un slice pequeño de H100; embeber &lt;strong>una query corta online&lt;/strong> cuesta decenas de ms, pero rerankear el &lt;strong>top-50&lt;/strong> cuesta &lt;strong>lineal en candidatos&lt;/strong> porque un cross-encoder evalúa cada par (query, doc) por separado. El reparto en el cluster 4×H100: &lt;strong>TEI-CPU&lt;/strong> en la flota para la ingesta batch (sin SLA, throughput/€), &lt;strong>TEI-GPU&lt;/strong> en un &lt;strong>slice MIG&lt;/strong> de una H100 para embeddings y rerank online de alto QPS, comunicados con la ingesta y con el gateway LiteLLM por el contrato OpenAI.&lt;/p>
&lt;h2 id="la-analogía-la-prensa-de-troquelar-que-solo-estampa-vectores">La analogía: la prensa de troquelar que solo estampa vectores&lt;/h2>
&lt;p>Imagina un taller con una &lt;strong>prensa de troquelar&lt;/strong> especializada. No fabrica piezas variadas; hace una sola cosa: coge material plano y, de un golpe, le estampa una forma. En nuestro caso, coge texto y estampa &lt;strong>un vector&lt;/strong>. Y como toda prensa industrial, tiene una propiedad que define todo su comportamiento: &lt;strong>el coste del golpe es casi fijo&lt;/strong>, da igual cuánto material metas en la bandeja.&lt;/p>
&lt;p>Bajar la prensa, calentar el troquel, alinear, prensar y levantar cuesta —pongamos— un segundo. Si metes &lt;strong>una sola&lt;/strong> lámina, gastas ese segundo entero en estampar una pieza: un desperdicio. Si llenas la bandeja con &lt;strong>treinta&lt;/strong> láminas y bajas la prensa una vez, gastas casi el mismo segundo y sales con treinta piezas estampadas. El coste por pieza se desploma. La prensa &lt;strong>rinde muchísimo más cuando llena la bandeja antes de prensar&lt;/strong>.&lt;/p>
&lt;p>Esto es exactamente el &lt;strong>dynamic batching&lt;/strong> de un servidor de embeddings. El &amp;ldquo;golpe&amp;rdquo; es el forward pass del modelo sobre la GPU o la CPU: una operación que carga los pesos, los multiplica por las activaciones y emite los vectores, con un coste fijo grande que no escala con cuántas secuencias proceses a la vez —hasta que saturas el ancho de banda de memoria o las unidades de cómputo—. Servir las peticiones de una en una es estampar una lámina por golpe. Agruparlas en un batch es llenar la bandeja.&lt;/p>
&lt;p>Pero la prensa tiene un dilema operativo, y aquí está el corazón del post. Si esperas a tener la bandeja &lt;strong>llena del todo&lt;/strong> antes de prensar, las primeras láminas que llegaron se quedan esperando a que lleguen las demás: &lt;strong>latencia&lt;/strong>. Si prensas en cuanto cae &lt;strong>una&lt;/strong> lámina para no hacer esperar a nadie, vuelves al desperdicio del golpe por pieza: &lt;strong>throughput bajo&lt;/strong>. El servidor de embeddings resuelve esto con una &lt;strong>ventana de espera acotada&lt;/strong>: junta lo que llegue en una ventana de microsegundos a milisegundos, o hasta llenar el cupo de tokens del batch, y entonces prensa. Es la política que convierte un montón de peticiones independientes en pocos golpes eficientes sin que nadie espere de más.&lt;/p>
&lt;p>El resto del post es, en el fondo, configurar bien esa prensa: cuántas láminas caben en la bandeja (&lt;code>max-batch-tokens&lt;/code>), cuánta cola se tolera antes de rechazar trabajo (&lt;code>max-concurrent-requests&lt;/code>), si la prensa vive en la flota barata de CPU (ingesta nocturna, throughput puro) o en un slice de la GPU cara (online, latencia acotada), y por qué el reranker es una prensa distinta cuyo coste &lt;strong>sí&lt;/strong> crece con el número de piezas.&lt;/p>
&lt;h2 id="qué-es-tei-exactamente">Qué es TEI, exactamente&lt;/h2>
&lt;p>&lt;strong>Text Embeddings Inference (TEI)&lt;/strong> es el servidor de inferencia de Hugging Face para la familia de modelos que &lt;strong>no genera tokens&lt;/strong>: embedders, rerankers cross-encoder y clasificadores de secuencias. Es a los encoders lo que vLLM es a los LLM generativos: un servidor de alto rendimiento, escrito en &lt;strong>Rust&lt;/strong>, que se ocupa del tokenizado, el batching, la concurrencia, las métricas y el contrato HTTP, dejando que el código de aplicación solo haga llamadas (&lt;a href="https://github.com/huggingface/text-embeddings-inference">repo TEI&lt;/a>).&lt;/p>
&lt;p>Tres tipos de modelo, tres trabajos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Embedding&lt;/strong> (bi-encoder): convierte un texto en un vector reutilizable. &lt;code>bge-m3&lt;/code>, &lt;code>Snowflake Arctic Embed&lt;/code>, &lt;code>multilingual-e5&lt;/code>. El vector se guarda en el índice y se reutiliza en cada búsqueda.&lt;/li>
&lt;li>&lt;strong>Reranker&lt;/strong> (cross-encoder): toma &lt;strong>un par&lt;/strong> (query, documento) y emite &lt;strong>un score de relevancia&lt;/strong>, no un vector. &lt;code>bge-reranker-v2-m3&lt;/code>. No produce nada reutilizable: cada par se evalúa de nuevo.&lt;/li>
&lt;li>&lt;strong>Clasificación de secuencias&lt;/strong>: emite etiquetas/probabilidades sobre un texto. Útil para guardrails ligeros, routing, detección de idioma o toxicidad.&lt;/li>
&lt;/ul>
&lt;h3 id="los-endpoints">Los endpoints&lt;/h3>
&lt;p>TEI expone tanto un endpoint &lt;strong>OpenAI-compatible&lt;/strong> como sus endpoints nativos, verificados contra la documentación actual (&lt;a href="https://huggingface.co/docs/text-embeddings-inference/quick_tour">Quick Tour&lt;/a>, &lt;a href="https://deepwiki.com/huggingface/text-embeddings-inference">DeepWiki&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Endpoint&lt;/th>
&lt;th>Para qué&lt;/th>
&lt;th>Tipo de modelo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/v1/embeddings&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Embeddings con el &lt;strong>contrato OpenAI&lt;/strong> (drop-in para clientes que ya hablan OpenAI)&lt;/td>
&lt;td>embedder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/embed&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Embeddings dense, API nativa de TEI&lt;/td>
&lt;td>embedder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/embed_sparse&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Embeddings &lt;strong>sparse&lt;/strong> (cabeza léxica de modelos tipo &lt;code>bge-m3&lt;/code>/SPLADE)&lt;/td>
&lt;td>embedder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/rerank&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Ordena una lista de textos por relevancia frente a una query&lt;/td>
&lt;td>reranker (cross-encoder)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/predict&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Clasificación de secuencias (etiquetas/scores)&lt;/td>
&lt;td>clasificador&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/embed_all&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Embeddings por token (sin pooling)&lt;/td>
&lt;td>embedder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/similarity&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Similitud directa entre textos&lt;/td>
&lt;td>embedder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/metrics&lt;/code>, &lt;code>/health&lt;/code>, &lt;code>/info&lt;/code>, &lt;code>/tokenize&lt;/code>, &lt;code>/decode&lt;/code>&lt;/td>
&lt;td>Operación, observabilidad y utilidades&lt;/td>
&lt;td>todos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La clave operativa está en la primera fila: que TEI hable el &lt;strong>contrato OpenAI&lt;/strong> en &lt;code>/v1/embeddings&lt;/code> significa que el embedder es &lt;strong>sustituible sin tocar el código cliente&lt;/strong>. El mismo código que llamaba a &lt;code>text-embedding-3-large&lt;/code> de OpenAI apunta a tu TEI on-prem cambiando la &lt;code>base_url&lt;/code>, y el gateway (&lt;a href="https://blog.lo0.es/posts/elegir-gateway-oss-inferencia-llm/">LiteLLM&lt;/a>) lo enruta como un proveedor más. El &lt;code>/rerank&lt;/code>, en cambio, &lt;strong>no&lt;/strong> forma parte del estándar OpenAI —no existe tal endpoint en su API— así que el gateway lo trata como un endpoint específico de reranking (&lt;a href="https://github.com/BerriAI/litellm/issues/8372">LiteLLM TEI rerank&lt;/a>).&lt;/p>
&lt;h3 id="los-backends">Los backends&lt;/h3>
&lt;p>TEI corre en CPU y en GPU con el &lt;strong>mismo contrato HTTP&lt;/strong>, lo que es exactamente la propiedad que el &lt;a href="https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/">reparto CPU/GPU del RAG&lt;/a> necesita (&lt;a href="https://huggingface.co/docs/text-embeddings-inference/index">docs TEI, hardware&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CPU&lt;/strong>: imagen &lt;code>cpu-*&lt;/code>. Backend &lt;strong>ONNX Runtime&lt;/strong> (recomendado) o &lt;strong>Intel MKL&lt;/strong>. Aprovecha las rutas de cómputo entero (AVX-512+VNNI, AMX en Xeon de 4ª gen) cuando el modelo está en int8. Es el motor de la ingesta batch.&lt;/li>
&lt;li>&lt;strong>CUDA&lt;/strong>: imágenes específicas por arquitectura (&lt;code>cuda-*&lt;/code>, con variantes para distintas compute capabilities). Usa &lt;strong>FlashAttention&lt;/strong> y &lt;strong>cuda graphs&lt;/strong> para exprimir la GPU. Requiere compute capability ≥ 7.5 (Volta queda fuera) y drivers compatibles con CUDA 12.2+. Es el motor del online de alto QPS.&lt;/li>
&lt;li>También hay soporte &lt;strong>Metal&lt;/strong> (Apple Silicon) y experimental &lt;strong>ROCm&lt;/strong> (AMD Instinct), menos relevantes para el caso on-prem de este post.&lt;/li>
&lt;/ul>
&lt;p>La regla de oro: &lt;strong>un mismo &lt;code>bge-m3&lt;/code> servido por TEI-CPU y por TEI-GPU expone &lt;code>/v1/embeddings&lt;/code> idéntico&lt;/strong>. El cliente no se entera del silicio. Eso es lo que permite poner la ingesta en CPU y el online en GPU sin reescribir nada.&lt;/p>
&lt;h2 id="por-qué-un-servidor-dedicado-y-no-llamar-al-modelo-inline">Por qué un servidor dedicado y no llamar al modelo inline&lt;/h2>
&lt;p>La tentación, sobre todo en el prototipo, es cargar el embedder en el proceso de la aplicación y llamar a &lt;code>model.encode()&lt;/code> directamente. Funciona en el demo y se rompe en producción por cinco razones, todas resueltas por el servidor dedicado:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Batching.&lt;/strong> Tu código de ingesta procesa documentos; tu código de query embebe una query a la vez. Inline, cada &lt;code>encode()&lt;/code> es un forward independiente —un golpe de prensa por lámina—. Un servidor dedicado &lt;strong>junta peticiones de orígenes distintos&lt;/strong> (ingesta + varias queries concurrentes) en un solo batch y amortiza el coste fijo. Esto es lo que de verdad multiplica el throughput, y no lo puedes hacer si el modelo vive dentro de cada proceso aislado.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Concurrencia y backpressure.&lt;/strong> Bajo carga, ¿qué pasa cuando llegan 10.000 peticiones a la vez? Inline, te quedas sin memoria o encolas sin control. TEI tiene &lt;code>--max-concurrent-requests&lt;/code>: por encima de ese límite &lt;strong>rechaza&lt;/strong> en vez de degradar a todos, que es la forma correcta de gestionar la sobrecarga (&lt;a href="https://github.com/huggingface/text-embeddings-inference/blob/main/docs/source/en/cli_arguments.md">CLI args TEI&lt;/a>).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Métricas.&lt;/strong> TEI expone &lt;code>/metrics&lt;/code> en formato Prometheus: latencias, tamaño de batch, tokens/s, peticiones en cola. Inline no tienes nada de esto sin instrumentar a mano cada &lt;code>encode()&lt;/code>. Para &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">observar el plano de datos&lt;/a> necesitas ese endpoint.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Un contrato HTTP reutilizable.&lt;/strong> El mismo servidor lo consume la ingesta batch, el query-time online y cualquier otro servicio que necesite vectores. Una sola copia del modelo en memoria, un solo punto de versión, un solo lugar donde cambiar el modelo. Inline, cada servicio carga su propia copia y la versión deriva sola.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Separación de ciclo de vida.&lt;/strong> El embedder se actualiza, se reinicia o se escala &lt;strong>sin tocar&lt;/strong> la aplicación. Si mañana cambias &lt;code>bge-m3&lt;/code> por &lt;code>Snowflake Arctic&lt;/code>, cambias el contenedor del servidor, no el código de N servicios. (Recuerda la trampa del &lt;a href="https://blog.lo0.es/posts/embeddings-modelos-2026-dense-sparse-multivector/">drift del embedder&lt;/a>: cambiar de embedder obliga a reindexar; tener un único servidor hace ese cambio atómico y auditable.)&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Es el mismo argumento por el que nadie sirve un LLM con &lt;code>transformers.generate()&lt;/code> en producción: se sirve con un motor dedicado. El embedder no es distinto.&lt;/p>
&lt;h2 id="dynamic-batching-la-mecánica-de-la-prensa">Dynamic batching: la mecánica de la prensa&lt;/h2>
&lt;p>Aquí está el motor del rendimiento. TEI hace &lt;strong>token-based dynamic batching&lt;/strong>: en vez de procesar peticiones de una en una o agruparlas por número fijo de peticiones, las agrupa por &lt;strong>presupuesto de tokens&lt;/strong> (&lt;a href="https://github.com/huggingface/text-embeddings-inference/blob/main/README.md">README TEI&lt;/a>, &lt;a href="https://github.com/huggingface/text-embeddings-inference/discussions/367">Discussion #367&lt;/a>).&lt;/p>
&lt;p>Dos perillas mandan:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>--max-batch-tokens&lt;/code>&lt;/strong> (por defecto depende de la build, típicamente 16384): el &lt;strong>total de tokens&lt;/strong> que caben en un batch. Con &lt;code>max-batch-tokens=1000&lt;/code> te entran 10 peticiones de 100 tokens, o una de 1000. La doc lo dice claro: este número &lt;em>debería ser el mayor posible hasta que el modelo se vuelva compute-bound&lt;/em>. Es el tamaño de la bandeja de la prensa, &lt;strong>medido en tokens, no en piezas&lt;/strong> —porque el coste real escala con tokens, no con número de textos—.&lt;/li>
&lt;li>&lt;strong>&lt;code>--max-concurrent-requests&lt;/code>&lt;/strong> (por defecto 512): cuántas peticiones admite en vuelo antes de rechazar. Es el control de &lt;strong>backpressure&lt;/strong>, no de batching: protege de avalanchas devolviendo error en vez de encolar indefinidamente.&lt;/li>
&lt;/ul>
&lt;p>La política: TEI mantiene una &lt;strong>cola interna&lt;/strong>. Cuando hay trabajo, llena un batch hasta el cupo de &lt;code>max-batch-tokens&lt;/code> (o hasta que no hay más peticiones esperando) y lo manda al modelo de un golpe. Las peticiones que llegan mientras el batch anterior se procesa esperan en la cola y entran en el siguiente. Cuidado con un detalle operativo real: si &lt;code>max-batch-tokens&lt;/code> es &lt;strong>menor&lt;/strong> que la longitud máxima de entrada del modelo, puedes provocar un bucle donde una petición larga nunca cabe en un batch (&lt;a href="https://github.com/huggingface/text-embeddings-inference/issues/723">Issue #723&lt;/a>); el cupo de tokens tiene que ser al menos tan grande como la secuencia más larga que admitas.&lt;/p>
&lt;h3 id="el-número-throughput-con-batching-vs-sin-batching">El número: throughput con batching vs sin batching&lt;/h3>
&lt;p>Modelicemos la prensa. Sea $C$ el coste fijo de un forward pass (cargar pesos, lanzar kernels) y $c$ el coste marginal por secuencia dentro del batch. Para un batch de tamaño $B$, el tiempo del golpe es aproximadamente:&lt;/p>
&lt;p>$$t(B) \approx C + c \cdot B$$&lt;/p>
&lt;p>mientras el batch quepa en el hardware (memory-bandwidth-bound, que es el régimen normal de un encoder pequeño en GPU). El &lt;strong>throughput&lt;/strong> —secuencias por segundo— es:&lt;/p>
&lt;p>$$\text{throughput}(B) = \frac{B}{t(B)} = \frac{B}{C + c \cdot B}$$&lt;/p>
&lt;p>Pongamos números concretos, orientativos pero del orden correcto para &lt;code>bge-m3&lt;/code> (568M) en una GPU sirviendo secuencias de ~256 tokens. Digamos $C = 4$ ms de coste fijo por golpe y $c = 0.5$ ms por secuencia marginal.&lt;/p>
&lt;p>&lt;strong>Sin batching&lt;/strong> ($B=1$):&lt;/p>
&lt;p>$$t(1) = 4 + 0.5 = 4.5 \text{ ms} \quad\Rightarrow\quad \text{throughput} = \frac{1}{4.5\text{ms}} \approx 222 \text{ sec/s}$$&lt;/p>
&lt;p>&lt;strong>Con batching&lt;/strong> ($B=32$):&lt;/p>
&lt;p>$$t(32) = 4 + 0.5 \times 32 = 20 \text{ ms} \quad\Rightarrow\quad \text{throughput} = \frac{32}{20\text{ms}} = 1600 \text{ sec/s}$$&lt;/p>
&lt;p>El batch de 32 multiplica el throughput por &lt;strong>~7.2×&lt;/strong> respecto a servir de una en una, porque el coste fijo $C$ —que con $B=1$ era el 89% del tiempo— se reparte entre 32. Y el límite asintótico, cuando $B \to \infty$, es $1/c = 2000$ sec/s: la prensa no puede ir más rápido que su coste marginal, y nos quedamos en el 80% de ese techo ya con $B=32$. Subir a $B=128$ daría ~1969 sec/s, una mejora marginal a cambio de bastante más latencia y memoria. &lt;strong>El rendimiento del batching tiene rendimientos decrecientes&lt;/strong>: la mayor parte de la ganancia está en pasar de 1 a unas decenas.&lt;/p>
&lt;p>El &lt;strong>trade-off de latencia&lt;/strong> es la otra cara. La petición que llegó &lt;strong>primera&lt;/strong> al batch de 32 no ve su respuesta hasta que el golpe entero termina: espera lo que tarda en llenarse la bandeja &lt;strong>más&lt;/strong> los 20 ms del forward. Si las 32 peticiones llegaron en una ventana de, digamos, 5 ms, la primera petición sufre ~5 ms de espera de batching + 20 ms de cómputo = &lt;strong>25 ms&lt;/strong> de latencia, frente a los 4.5 ms que habría tenido sirviéndose sola. La latencia &lt;strong>p99 empeora&lt;/strong> —es el precio de la bandeja llena—, pero a cambio el sistema absorbe &lt;strong>7× más carga&lt;/strong> con el mismo silicio. Por eso la configuración correcta &lt;strong>depende del caso de uso&lt;/strong>: en ingesta batch (sin usuario esperando) maximizas &lt;code>max-batch-tokens&lt;/code> y te da igual la latencia; en online (usuario esperando) acotas la ventana de espera para mantener la p99 bajo control aunque sacrifiques algo de throughput.&lt;/p>
&lt;div class="diagram" style="max-width:840px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 840 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Dynamic batching en TEI: peticiones que llegan, cola, batch por tokens, forward, respuestas">
&lt;text x="420" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Dynamic batching por tokens en TEI&lt;/text>
&lt;text x="80" y="60" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Peticiones&lt;/text>
&lt;text x="80" y="76" text-anchor="middle" font-size="10" fill="currentColor">llegan por separado&lt;/text>
&lt;rect x="30" y="92" width="100" height="22" rx="4" fill="none" stroke="#3b82f6" stroke-width="1.2"/>
&lt;text x="80" y="107" text-anchor="middle" font-size="10" fill="currentColor">query A · 80 tok&lt;/text>
&lt;rect x="30" y="120" width="100" height="22" rx="4" fill="none" stroke="#3b82f6" stroke-width="1.2"/>
&lt;text x="80" y="135" text-anchor="middle" font-size="10" fill="currentColor">query B · 120 tok&lt;/text>
&lt;rect x="30" y="148" width="100" height="22" rx="4" fill="none" stroke="#3b82f6" stroke-width="1.2"/>
&lt;text x="80" y="163" text-anchor="middle" font-size="10" fill="currentColor">ingesta · 256 tok&lt;/text>
&lt;rect x="30" y="176" width="100" height="22" rx="4" fill="none" stroke="#3b82f6" stroke-width="1.2"/>
&lt;text x="80" y="191" text-anchor="middle" font-size="10" fill="currentColor">ingesta · 256 tok&lt;/text>
&lt;rect x="30" y="204" width="100" height="22" rx="4" fill="none" stroke="#3b82f6" stroke-width="1.2"/>
&lt;text x="80" y="219" text-anchor="middle" font-size="10" fill="currentColor">query C · 60 tok&lt;/text>
&lt;path d="M135,150 L185,150" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#tb)"/>
&lt;rect x="190" y="80" width="150" height="160" rx="6" fill="#f59e0b" opacity="0.12" stroke="#f59e0b" stroke-width="1.4"/>
&lt;text x="265" y="102" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Cola interna&lt;/text>
&lt;text x="265" y="120" text-anchor="middle" font-size="10" fill="currentColor">llena bandeja hasta&lt;/text>
&lt;text x="265" y="134" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">max-batch-tokens&lt;/text>
&lt;rect x="210" y="146" width="110" height="18" rx="3" fill="none" stroke="#f59e0b" stroke-width="1"/>
&lt;rect x="210" y="168" width="110" height="18" rx="3" fill="none" stroke="#f59e0b" stroke-width="1"/>
&lt;rect x="210" y="190" width="110" height="18" rx="3" fill="none" stroke="#f59e0b" stroke-width="1"/>
&lt;text x="265" y="226" text-anchor="middle" font-size="10" font-style="italic" fill="currentColor">ventana acotada o cupo lleno&lt;/text>
&lt;path d="M345,160 L395,160" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#tb)"/>
&lt;text x="370" y="152" text-anchor="middle" font-size="9" fill="currentColor">batch&lt;/text>
&lt;rect x="400" y="110" width="180" height="100" rx="6" fill="#ef4444" opacity="0.10" stroke="#ef4444" stroke-width="1.6"/>
&lt;text x="490" y="134" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">UN forward pass&lt;/text>
&lt;text x="490" y="152" text-anchor="middle" font-size="10" fill="currentColor">GPU (CUDA, FlashAttn)&lt;/text>
&lt;text x="490" y="166" text-anchor="middle" font-size="10" fill="currentColor">o CPU (ONNX/AMX int8)&lt;/text>
&lt;text x="490" y="184" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">coste C + c·B&lt;/text>
&lt;text x="490" y="200" text-anchor="middle" font-size="9" font-style="italic" fill="currentColor">"un golpe de prensa"&lt;/text>
&lt;path d="M585,160 L635,160" stroke="currentColor" stroke-width="1.4" fill="none" marker-end="url(#tb)"/>
&lt;text x="730" y="100" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Respuestas&lt;/text>
&lt;rect x="660" y="112" width="140" height="20" rx="4" fill="none" stroke="#22c55e" stroke-width="1.2"/>
&lt;text x="730" y="126" text-anchor="middle" font-size="10" fill="currentColor">vector A&lt;/text>
&lt;rect x="660" y="138" width="140" height="20" rx="4" fill="none" stroke="#22c55e" stroke-width="1.2"/>
&lt;text x="730" y="152" text-anchor="middle" font-size="10" fill="currentColor">vector B&lt;/text>
&lt;rect x="660" y="164" width="140" height="20" rx="4" fill="none" stroke="#22c55e" stroke-width="1.2"/>
&lt;text x="730" y="178" text-anchor="middle" font-size="10" fill="currentColor">vectores ingesta&lt;/text>
&lt;rect x="660" y="190" width="140" height="20" rx="4" fill="none" stroke="#22c55e" stroke-width="1.2"/>
&lt;text x="730" y="204" text-anchor="middle" font-size="10" fill="currentColor">vector C&lt;/text>
&lt;line x1="30" y1="270" x2="800" y2="270" stroke="currentColor" stroke-width="0.8"/>
&lt;text x="30" y="292" font-size="11" font-weight="700" fill="currentColor">El trade-off:&lt;/text>
&lt;text x="30" y="312" font-size="11" fill="currentColor">batch grande → throughput alto (coste fijo C repartido entre B) pero p99 peor (la 1.ª espera a llenar)&lt;/text>
&lt;text x="30" y="330" font-size="11" fill="currentColor">batch=1 → latencia mínima por petición pero throughput pésimo (un golpe por lámina)&lt;/text>
&lt;text x="30" y="352" font-size="11" font-style="italic" fill="currentColor">Ingesta: maximiza max-batch-tokens. Online: acota la ventana de espera para proteger la p99.&lt;/text>
&lt;defs>&lt;marker id="tb" 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;/svg>
&lt;/div>
&lt;h2 id="deployment-compose-para-embedder-y-reranker">Deployment: compose para embedder y reranker&lt;/h2>
&lt;p>TEI se despliega como contenedor. Los argumentos clave: &lt;code>--model-id&lt;/code> (el modelo del Hub), &lt;code>--pooling&lt;/code> (cómo agrega los tokens en el vector: &lt;code>cls&lt;/code>, &lt;code>mean&lt;/code>, &lt;code>splade&lt;/code>, &lt;code>last-token&lt;/code>), &lt;code>--dtype&lt;/code> (precisión), y las perillas de batching ya vistas. Dos servicios distintos —el embedder y el reranker son &lt;strong>modelos y endpoints distintos&lt;/strong>—, cada uno con su contenedor.&lt;/p>
&lt;h3 id="embedder-bge-m3-en-gpu">Embedder (&lt;code>bge-m3&lt;/code>) en GPU&lt;/h3>
&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: TEI sirviendo bge-m3 como embedder, en 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:cuda-1.9&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">model-id=BAAI/bge-m3&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="l">pooling=cls&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="l">dtype=float16&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">--max-batch-tokens=16384 # bandeja grande&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">la prensa rinde&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">--max-concurrent-requests=512 # backpressure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rechaza por encima&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="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;./hf-cache:/data&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">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;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># expone /v1/embeddings (OpenAI), /embed, /embed_sparse, /metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="reranker-bge-reranker-v2-m3-en-gpu">Reranker (&lt;code>bge-reranker-v2-m3&lt;/code>) en GPU&lt;/h3>
&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="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:cuda-1.9&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">model-id=BAAI/bge-reranker-v2-m3 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># cross-encoder, ~568M&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="l">dtype=float16&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="l">max-batch-tokens=16384&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="l">max-concurrent-requests=256&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="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;./hf-cache:/data&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">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;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># expone /rerank — se invoca SOLO sobre top-k (20/50), no sobre cientos&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>bge-reranker-v2-m3&lt;/code> es un &lt;strong>cross-encoder de ~568M&lt;/strong> construido sobre &lt;code>bge-m3&lt;/code>, multilingüe, que toma (query, documento) y emite un score de relevancia directamente, sin producir vector reutilizable (&lt;a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">model card&lt;/a>, &lt;a href="https://bge-model.com/bge/bge_reranker_v2.html">BGE docs&lt;/a>). No lleva &lt;code>--pooling&lt;/code> porque no emite embedding: emite un escalar.&lt;/p>
&lt;h3 id="cpu-para-la-ingesta-batch">CPU para la ingesta batch&lt;/h3>
&lt;p>Para la flota CPU, basta cambiar la imagen y el dtype; el contrato HTTP es idéntico:&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="w"> &lt;/span>&lt;span class="nt">tei-embed-cpu&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-1.6&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">model-id=BAAI/bge-m3&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="l">pooling=cls&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="l">dtype=float16 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># en CPU, ONNX int8 si el export 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 class="nt">--max-batch-tokens=8192 # bandeja menor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">la CPU satura antes&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;8083: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="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;./hf-cache:/data&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 en Xeon de 4ª gen&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Un cliente OpenAI cualquiera embebe contra el endpoint sin saber qué hay detrás:&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">openai&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OpenAI&lt;/span>
&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">OpenAI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://tei-embed:80/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;-&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># api_key ignorada&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="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">embeddings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;BAAI/bge-m3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">input&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;consulta del usuario&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">vec&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">r&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">embedding&lt;/span> &lt;span class="c1"># mismo contrato que OpenAI; el silicio es invisible&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y el rerank, por su endpoint nativo (no OpenAI):&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">import&lt;/span> &lt;span class="nn">requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">docs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;chunk recuperado 1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;chunk recuperado 2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;...top-20 del retrieval...&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">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">requests&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;http://tei-rerank:80/rerank&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">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;consulta del usuario&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;texts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">docs&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ranked&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">r&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># [{index, score}, ...] ordenado por relevancia&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="integración-cómo-lo-consumen-la-ingesta-y-el-gateway">Integración: cómo lo consumen la ingesta y el gateway&lt;/h2>
&lt;p>El servidor TEI tiene &lt;strong>dos clientes&lt;/strong> con perfiles opuestos, y ese es justo el motivo de centralizarlo:&lt;/p>
&lt;p>&lt;strong>La ingesta&lt;/strong> (batch, sin SLA) llama a &lt;code>/v1/embeddings&lt;/code> o &lt;code>/embed&lt;/code> con &lt;strong>lotes grandes&lt;/strong> de chunks. Aquí interesa el throughput: la ingesta empuja miles de textos y TEI los agrupa en batches grandes contra el &lt;code>max-batch-tokens&lt;/code> alto. Es el caso de la prensa con la bandeja llena. La ingesta luego escribe los vectores (dense + sparse) en el vector DB —el pipeline concreto de PDF a chunk indexado es la pieza hermana en preparación de esta serie; mientras, el &lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">esquema PostgreSQL + Qdrant en microservicios&lt;/a> cubre la estructura del pipeline—.&lt;/p>
&lt;p>&lt;strong>El gateway&lt;/strong> (query-time, online) consume TEI por dos rutas distintas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Embeber la query&lt;/strong>: una sola llamada a &lt;code>/v1/embeddings&lt;/code> con un texto corto. El gateway —&lt;a href="https://blog.lo0.es/posts/elegir-gateway-oss-inferencia-llm/">LiteLLM&lt;/a> u otro router L7— lo trata como un proveedor de embeddings OpenAI más, enrutado por modelo. Esa query embebida va al vector DB para la búsqueda híbrida.&lt;/li>
&lt;li>&lt;strong>Rerankear el top-k&lt;/strong>: tras recuperar 20-50 candidatos del retriever, el gateway llama a &lt;code>/rerank&lt;/code> del servidor reranker con la query y la lista de candidatos, y se queda con los mejores. Como &lt;code>/rerank&lt;/code> no es estándar OpenAI, LiteLLM lo expone por su contrato de reranking propio (&lt;a href="https://github.com/BerriAI/litellm/issues/8372">LiteLLM rerank con TEI&lt;/a>).&lt;/li>
&lt;/ol>
&lt;p>El patrón completo en query-time: &lt;em>embeber query (TEI dense) → buscar (vector DB, &lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">hybrid retrieval&lt;/a>) → rerankear top-k (TEI rerank) → mandar query + top-5 al LLM&lt;/em>. Dos endpoints TEI distintos —uno para dense, otro para rerank—, ambos detrás del gateway, ambos sustituibles.&lt;/p>
&lt;h2 id="dimensionado-memoria-réplicas-y-presupuesto-de-latencia">Dimensionado: memoria, réplicas y presupuesto de latencia&lt;/h2>
&lt;h3 id="footprint-de-un-encoder-de-568m">Footprint de un encoder de 568M&lt;/h3>
&lt;p>El footprint de pesos de &lt;code>bge-m3&lt;/code> (568M params) depende solo de la precisión:&lt;/p>
&lt;p>$$\text{fp16: } 568 \times 10^6 \times 2 \text{ B} \approx 1.14 \text{ GB} \qquad \text{int8: } 568 \times 10^6 \times 1 \text{ B} \approx 0.57 \text{ GB}$$&lt;/p>
&lt;p>A esto se suma el espacio de &lt;strong>activaciones temporales&lt;/strong> del batch (proporcional a &lt;code>max-batch-tokens&lt;/code> × dimensión oculta) y, en TEI, las estructuras de FlashAttention/cuda graphs. En la práctica, un &lt;code>bge-m3&lt;/code> en fp16 se sirve cómodamente en &lt;strong>~6 GB de VRAM&lt;/strong> incluso a batch alto, y en int8 en CPU el modelo entero (0.57 GB) cabe en caché y RAM de cualquier servidor sin pestañear, activando las rutas AMX/VNNI que lo hacen viable —exactamente el argumento del &lt;a href="https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/">RAG en CPU&lt;/a>—.&lt;/p>
&lt;p>&lt;strong>¿Cuántas réplicas caben?&lt;/strong> Si reservamos un slice de &lt;strong>10 GB&lt;/strong> de una H100 para servir embeddings online, con un footprint efectivo de ~2.5 GB por réplica (pesos fp16 + activaciones + overhead), caben del orden de &lt;strong>3-4 réplicas&lt;/strong> de &lt;code>bge-m3&lt;/code> en ese slice, repartiendo el QPS entre ellas. En int8 el footprint baja y caben más, a costa de un punto largo de calidad. Para el reranker, el footprint de pesos es el mismo (568M), pero su patrón de batch es distinto (pares query-doc), así que conviene dimensionarlo por separado.&lt;/p>
&lt;h3 id="presupuesto-de-latencia-query-corta-vs-rerank-de-top-50">Presupuesto de latencia: query corta vs rerank de top-50&lt;/h3>
&lt;p>Aquí está la asimetría fundamental entre embedder y reranker, y es lo que decide el dimensionado.&lt;/p>
&lt;p>&lt;strong>Embeber una query corta&lt;/strong> (online): es &lt;strong>un&lt;/strong> forward sobre un texto de decenas de tokens. En GPU, dentro de un batch dinámico, son &lt;strong>decenas de ms&lt;/strong> incluyendo la espera de batching —el orden del ejemplo numérico de antes, ~5-25 ms—. Es barato y constante: una query es una query, no importa cuántos documentos haya en el corpus. Por eso embeber online cabe holgadamente en un presupuesto de latencia interactivo.&lt;/p>
&lt;p>&lt;strong>Rerankear el top-50&lt;/strong> (online): un cross-encoder &lt;strong>no&lt;/strong> produce un vector reutilizable; evalúa &lt;strong>cada par&lt;/strong> (query, documento) en un forward. El coste es &lt;strong>lineal en el número de candidatos&lt;/strong>:&lt;/p>
&lt;p>$$\text{coste}_{\text{rerank}} \propto k \times \text{forward}(\text{query} + \text{doc})$$&lt;/p>
&lt;p>Rerankear top-50 son &lt;strong>50 forwards&lt;/strong> de pares (query + doc), donde cada par es más largo que la query sola (query + chunk de ~256 tokens). TEI los agrupa en batches —ahí el batching ayuda otra vez—, pero el &lt;strong>trabajo total&lt;/strong> es ~50× el de embeber una query. Si embeber una query cuesta del orden de 10-20 ms, rerankear 50 candidatos cuesta del orden de &lt;strong>cientos de ms&lt;/strong> según el batch y el hardware. De ahí la regla operativa repetida en toda la serie: &lt;strong>recall amplio y barato en el retriever, rerank de precisión sobre POCOS candidatos&lt;/strong>. Rerankear top-20/50 cabe en el presupuesto online; rerankear cientos a alto QPS no, y ahí es donde el reranker reclama GPU dedicada o un slice más grande. El detalle de por qué el cross-encoder es caro pero preciso está en &lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">reranker e hybrid retrieval&lt;/a>.&lt;/p>
&lt;p>El presupuesto de latencia query-time, sumado:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Etapa&lt;/th>
&lt;th>Coste típico (orden)&lt;/th>
&lt;th>Escala con&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Embeber query (TEI dense)&lt;/td>
&lt;td>~10-20 ms&lt;/td>
&lt;td>constante (1 texto corto)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Búsqueda híbrida (vector DB, HNSW+sparse)&lt;/td>
&lt;td>single-digit ms&lt;/td>
&lt;td>tamaño del índice (sublineal)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rerank top-k (TEI cross-encoder)&lt;/td>
&lt;td>~100-300 ms&lt;/td>
&lt;td>&lt;strong>lineal en k&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Generación (LLM, fuera de TEI)&lt;/td>
&lt;td>segundos&lt;/td>
&lt;td>tokens de salida&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El reranker es la etapa más cara del plano de datos, y la única que escala mal con el ancho de la recuperación. Dimensiónalo como el componente crítico.&lt;/p>
&lt;h2 id="aplicado-al-cluster-genérico-4h100">Aplicado al cluster genérico 4×H100&lt;/h2>
&lt;p>Bajemos 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). TEI vive en &lt;strong>dos sitios&lt;/strong> según el perfil de carga, y el contrato OpenAI idéntico es lo que permite que cada sitio sea sustituible.&lt;/p>
&lt;p>&lt;strong>TEI-CPU en la flota → ingesta batch.&lt;/strong> El re-embebido del corpus es trabajo throughput-bound sin SLA de latencia: su sitio es la flota CPU con &lt;code>bge-m3&lt;/code> en int8 (ONNX/AMX). Aquí maximizas &lt;code>max-batch-tokens&lt;/code> porque nadie espera, y la prensa rinde con la bandeja a tope. Ninguna H100 debería gastar un ciclo re-indexando un corpus que cambia una vez al día —es exactamente el mal reparto que &lt;a href="https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/">el RAG en CPU&lt;/a> desmonta—. La flota CPU escala horizontal y barata; el corpus se trocea y se reparte entre nodos.&lt;/p>
&lt;p>&lt;strong>TEI-GPU en un slice MIG → embeddings y rerank online de alto QPS.&lt;/strong> El query-time tiene SLA de latencia y, si el sistema sirve muchas peticiones por segundo, necesita el throughput y la baja latencia de la GPU. Pero un encoder de 568M &lt;strong>no merece una H100 entera&lt;/strong>: ocupa ~2.5 GB efectivos, y dejarle 80 GB es desperdiciar el 97% de la tarjeta. La respuesta correcta es un &lt;strong>slice MIG&lt;/strong>: particionar una H100 en instancias aisladas por hardware y dar a TEI uno de los slices pequeños (un &lt;code>1g.10gb&lt;/code>, por ejemplo), dejando los slices grandes para la generación o para otros tenants. El embedder y el reranker online viven ahí, con aislamiento de memoria y cómputo garantizado por el particionado. El cómo del particionado —MIG, MPS, time-slicing y cuándo usar cada uno— está en &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">compartir una GPU&lt;/a>; la lectura para TEI es directa: &lt;strong>MIG da el aislamiento duro que un servicio online de latencia acotada quiere&lt;/strong>, mientras que la ingesta batch en CPU ni siquiera toca la GPU.&lt;/p>
&lt;p>El reparto, en una frase: &lt;strong>la ingesta exprime throughput/€ en CPU con la bandeja llena; el online exprime latencia/QPS en un slice MIG de H100; ambos hablan el mismo &lt;code>/v1/embeddings&lt;/code> y &lt;code>/rerank&lt;/code>, así que mover carga de un lado a otro es cambiar una URL en el gateway.&lt;/strong>&lt;/p>
&lt;p>Si en algún momento necesitas un &lt;strong>embedder de 7B&lt;/strong> (gte-Qwen2, NV-Embed) que &lt;code>bge-m3&lt;/code> no alcanza en calidad, eso ya no es trabajo de encoder pequeño: arrastra el perfil de coste de un LLM y se sirve donde se sirven los 7B —con vLLM &lt;code>--task embed&lt;/code> en un slice grande de GPU—, no con TEI-CPU. Pero es la excepción puntual, no la carga base.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>TEI es la prensa de troquelar del plano de datos del RAG: un servidor especializado que solo estampa vectores y scores, y que &lt;strong>rinde muchísimo más cuando llena la bandeja antes de prensar&lt;/strong>. La pieza técnica que da el rendimiento es el batching dinámico por tokens, controlado por &lt;code>max-batch-tokens&lt;/code> y &lt;code>max-concurrent-requests&lt;/code>, con un trade-off claro entre throughput y latencia p99 que se resuelve distinto en ingesta (bandeja a tope, sin SLA) y en online (ventana acotada, p99 protegida). La virtud arquitectónica es el contrato OpenAI-compatible en &lt;code>/v1/embeddings&lt;/code>, que hace el embedder sustituible sin tocar el código, más los endpoints nativos &lt;code>/embed_sparse&lt;/code> y &lt;code>/rerank&lt;/code> para la cabeza sparse de &lt;code>bge-m3&lt;/code> y para el cross-encoder &lt;code>bge-reranker-v2-m3&lt;/code>. Los números mandan el dimensionado: un encoder de 568M cabe en gigas, no en decenas de gigas, así que varias réplicas viven en un slice MIG; embeber una query es barato y constante, pero rerankear es lineal en candidatos y es la etapa crítica del presupuesto de latencia. En el cluster 4×H100, el reparto correcto es TEI-CPU para la ingesta batch y TEI-GPU en un slice MIG para el online de alto QPS —dos prensas, dos silicios, un solo contrato—.&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/rag-en-cpu-plano-datos-generacion/">Llevar el RAG a la CPU: separar el plano de datos del plano de generación&lt;/a> — por qué la ingesta de embeddings va a la flota CPU; TEI-CPU es el motor concreto de ese plano.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/embeddings-modelos-2026-dense-sparse-multivector/">Embeddings en 2026: dense, sparse y multivector&lt;/a> — qué modelo sirves con TEI y por qué &lt;code>bge-m3&lt;/code> es el default multilingüe; este post sirve lo que aquel elige.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">Reranking e hybrid retrieval: fundamentos&lt;/a> — por qué el cross-encoder es caro pero preciso, y la regla de rerankear pocos candidatos que dimensiona el &lt;code>/rerank&lt;/code> de TEI.&lt;/li>
&lt;li>&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) donde corre TEI-CPU para ingesta.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/semantic-cache-rag/">Caché semántico para RAG&lt;/a> — la capa que evita llamar a TEI cuando la query ya se respondió, ahorrando incluso el coste del embedding online.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU: time-slicing, MPS y MIG&lt;/a> — el particionado MIG donde vive TEI-GPU online, conviviendo con la generación en la misma H100.&lt;/li>
&lt;li>&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 que consume &lt;code>/v1/embeddings&lt;/code> de TEI para poblar el índice (pieza hermana).&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Hugging Face — &lt;em>Text Embeddings Inference (TEI)&lt;/em>, repo y docs (Rust, backends CPU ONNX/MKL y CUDA, endpoints OpenAI-compatibles). &lt;a href="https://github.com/huggingface/text-embeddings-inference">https://github.com/huggingface/text-embeddings-inference&lt;/a> · &lt;a href="https://huggingface.co/docs/text-embeddings-inference">https://huggingface.co/docs/text-embeddings-inference&lt;/a>&lt;/li>
&lt;li>Hugging Face — &lt;em>TEI Quick Tour&lt;/em> (endpoints &lt;code>/embed&lt;/code>, &lt;code>/embed_sparse&lt;/code>, &lt;code>/rerank&lt;/code>, &lt;code>/predict&lt;/code>, &lt;code>/v1/embeddings&lt;/code>). &lt;a href="https://huggingface.co/docs/text-embeddings-inference/quick_tour">https://huggingface.co/docs/text-embeddings-inference/quick_tour&lt;/a>&lt;/li>
&lt;li>Hugging Face — &lt;em>TEI CLI arguments&lt;/em> (&lt;code>--model-id&lt;/code>, &lt;code>--pooling&lt;/code>, &lt;code>--dtype&lt;/code>, &lt;code>--max-batch-tokens&lt;/code>, &lt;code>--max-concurrent-requests&lt;/code>). &lt;a href="https://github.com/huggingface/text-embeddings-inference/blob/main/docs/source/en/cli_arguments.md">https://github.com/huggingface/text-embeddings-inference/blob/main/docs/source/en/cli_arguments.md&lt;/a>&lt;/li>
&lt;li>Hugging Face — &lt;em>TEI README / Discussion #367&lt;/em> (token-based dynamic batching; semántica de &lt;code>max-batch-tokens&lt;/code>). &lt;a href="https://github.com/huggingface/text-embeddings-inference/blob/main/README.md">https://github.com/huggingface/text-embeddings-inference/blob/main/README.md&lt;/a> · &lt;a href="https://github.com/huggingface/text-embeddings-inference/discussions/367">https://github.com/huggingface/text-embeddings-inference/discussions/367&lt;/a>&lt;/li>
&lt;li>BAAI — &lt;em>BGE-M3 model card&lt;/em> (568M, XLM-RoBERTa, dense+sparse+colbert, 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 ~568M sobre bge-m3, multilingüe). &lt;a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">https://huggingface.co/BAAI/bge-reranker-v2-m3&lt;/a> · &lt;a href="https://bge-model.com/bge/bge_reranker_v2.html">https://bge-model.com/bge/bge_reranker_v2.html&lt;/a>&lt;/li>
&lt;li>Chen, J., et al. &lt;em>BGE M3-Embedding&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>BerriAI — &lt;em>LiteLLM: proveedor TEI para &lt;code>/rerank&lt;/code>&lt;/em>. &lt;a href="https://github.com/BerriAI/litellm/issues/8372">https://github.com/BerriAI/litellm/issues/8372&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>