Tokenizador Unigram de código abierto de Perplexity AI que logra una latencia p50 5 veces menor que la caja de tokenizadores de cara abrazada

El equipo de investigación de Perplexity AI reimplementó su tokenizador Unigram desde cero en Rust y abrió el código en pplx-garden, su repositorio de tecnología de inferencia.

En longitudes de entrada de producción, el nuevo codificador reduce la latencia p50 aproximadamente 5 veces en comparación con la caja de tokenizadores Hugging Face, ~2 veces en comparación con SentencePiece (C++) y ~1,5 veces en comparación con el tokenizador de IREE (C), con cero asignaciones de almacenamiento dinámico en estado estacionario. En producción, redujo la utilización de CPU en la pila de inferencia de Perplexity entre 5 y 6 veces y redujo en milisegundos de dos dígitos la latencia del reranker.

Por qué la tokenización se convirtió en un cuello de botella

El costo de inferencia de LLM generalmente se enmarca en el trabajo de GPU: cachés KV, núcleos de atención, enrutamiento experto. Pero los modelos más pequeños, como los modelos integrados, los clasificadores y los reordenadores, cuentan una historia diferente. Estos modelos son de dos a tres órdenes de magnitud más pequeños que los transformadores de frontera.

Un reclasificador que obtiene cientos de documentos de candidatos por solicitud es un claro ejemplo. Con un modelo pequeño, el cálculo de la GPU suele finalizar en milisegundos de un solo dígito. Cada entrada todavía pasa primero por la tokenización del lado de la CPU. Cuando el tamaño de los lotes es grande, la tokenización se convierte en una fracción significativa de la latencia total de las solicitudes.

El trabajo de Perplexity tiene como objetivo XLM-RoBERTa, un modelo con un vocabulario Unigram de 250.000 tokens entrenado con SentencePieza. Los codificadores de la familia RoBERTa ajustados son una opción de producción común para tareas de clasificación, recuperación y similitud.

¿Qué es la tokenización de Unigram?

Kudo introdujo la tokenización de Unigram en 2018 y se implementa en SentencePieza. Enmarca la segmentación como un problema de ruta más probable. Cada ficha de vocabulario tiene una probabilidad logarítmica aprendida. El tokenizador elige la segmentación cuyas puntuaciones de token suman el valor más alto.

El algoritmo utilizado para encontrar el mejor camino es el algoritmo de Viterbi, una técnica de programación dinámica de 1967. Las posiciones de los bytes forman capas de gráficos y los tokens de vocabulario son bordes que abarcan un rango de bytes contiguos. La recurrencia de DP itera sobre las posiciones de los bytes y actualiza la ruta de mejor puntuación en cada posición.

El bucle exterior se ejecuta en tiempo lineal en relación con la longitud de entrada. El bucle interno recorre un ensayo de vocabulario (una estructura de árbol de prefijos) en cada posición de byte. Con una entrada de 16.000 tokens, este paseo interior ejecuta cientos de miles de transiciones de prueba. Es el camino caliente.

¿Qué fue lento en la implementación de la cara de abrazo?

La caja de tokenizadores Hugging Face es el tokenizador de Rust predeterminado que utilizan la mayoría de los equipos. Perplejidad lo utilizó como referencia. Con 514 tokens (512 + inyección BOS/EOS), la implementación de referencia tenía tres patrones costosos:

Mecanismo de cuello de botella Impacto medido Asignación por coincidencia Cadena::from_utf8 + AHashMap búsqueda por coincidencia trie 7295 asignaciones en 514 tokens; 299,171 en 16KSeguimiento de punteros por byteAHashMap en cada nodo trie; 4 cargas dependientes por paso de byte La latencia de carga dependiente domina la ruta activa L2 golpeando en entradas largas Tabla DP y buffers de salida recién asignados en cada llamada La tasa de fallos L2 aumenta del 8% con 128 tokens al 50% con 16K

La asignación por token es constante: aproximadamente 2 KB y ~18 asignaciones por token, independientemente del tamaño de entrada. El problema de latencia se vuelve grave en entradas más largas cuando las asignaciones acumulativas desbordan la caché L2 por núcleo.

Establecer una línea de base antes de cambiar el Trie

Antes de cambiar la estructura trie, Perplexity primero aisló cuánto costo provenía únicamente del trabajo innecesario. Hicieron un puerto de asignación cero de la referencia: el mismo intento de HashMap, pero con una estructura temporal propiedad de la persona que llama reutilizada en las llamadas y los ID de token almacenados directamente en los nodos de prueba (eliminando la asignación de cadenas por coincidencia y la búsqueda de mapa hash secundario).

Esta línea de base ya redujo la latencia de p50 a 155 µs en 514 tokens, frente a los 326 µs de la referencia. Las instrucciones retiradas cayeron 2,4x. El costo restante fue la propia búsqueda del puntero HashMap, que se abordó en el siguiente paso.

Las tres optimizaciones

Optimización 1: Trie de doble matriz

El experimento Hugging Face almacena a los niños en un HashMap en cada nodo. Cada paso de byte requiere un cálculo hash, dos desreferencias de puntero y un acceso al montón. Perplexity reemplazó esto con un trie de doble matriz, la misma estructura utilizada por SentencePiece e IREE, introducida originalmente por Aoe en 1989.

Un trie de doble matriz codifica el trie completo en dos matrices de enteros planos, base y check. Una búsqueda secundaria es: siguiente = base[nodo] + byte, luego verificar verificar[siguiente] == nodo. Es decir, dos lecturas de matriz, una suma de enteros y una comparación, sin hash ni persecución de punteros. Para el vocabulario de 250K de XLM-RoBERTa, todo el conjunto cabe en ~9 MB de memoria contigua. El conjunto de trabajo en caliente por codificación es del orden de 100 KB, que cabe en la caché L2.

A diferencia de SentencePiece e IREE, que son bibliotecas de uso general con contabilidad de celosía y canalizaciones de múltiples etapas, Perplexity incorporó el trie directamente en el bucle de Viterbi y eliminó esa sobrecarga por completo.

Resultado en 514 tokens: p50 cayó de 155 µs (línea de base de asignación cero) a 68 µs. El reloj de pared cayó 4,8 veces respecto a la referencia original.

Optimización 2: mapa de bits y empaquetado en línea

La prueba de doble matriz todavía requiere dos cargas de matriz dependientes por paso de byte: primero el desplazamiento base del padre, luego la matriz de verificación para confirmar que la transición es válida. Perplexity reemplazó la matriz de verificación con un mapa de bits por nodo (cuatro palabras de 64 bits, 32 bytes) que registra cuáles de los 256 bytes posibles tienen transiciones secundarias válidas.

Una búsqueda de mapa de bits se compila en una prueba de un solo bit con una palabra de 64 bits. La matriz de verificación se usa solo durante la construcción de prueba y se elimina por completo del diseño de tiempo de ejecución.

También empaquetaron los cuatro campos por nodo (mapa de bits, base, ID de token y puntuación) en una única línea de caché de 64 bytes, coincidiendo exactamente con el ancho de la línea de caché de la CPU. Un paso de prueba ahora carga una única línea de caché que cubre el mapa de bits para la verificación del siguiente byte, el desplazamiento base para la ranura secundaria y el ID del token y la puntuación en los nodos terminales.

Compensación: el tamaño del trie aumenta de ~9 MB a ~50 MB (780 000 nodos x 64 bytes). El conjunto de trabajo en caliente por codificación sigue siendo ~100 KB.

Resultado en 514 tokens: Reducción adicional del 4,5% del reloj de pared. Los accesos L2 cayeron de 4,6K a 1,8K por codificación.

Optimización 3: páginas enormes para Trie

Con 50 MB, el trie abarca aproximadamente 12.000 páginas virtuales en un sistema Linux predeterminado que utiliza páginas de 4 KB. El TLB de datos de primer nivel en Intel Sapphire Rapids tiene 96 entradas. Cada paso de Viterbi toca un nodo trie diferente, por lo que los errores de TLB se acumulan. En una codificación de 512 tokens, Perplexity estimó aproximadamente 9000 ciclos gastados en recorridos por tablas de páginas, aproximadamente el 3% del presupuesto por codificación.

Perplexity respaldó el trie con páginas enormes de 2 MB a través de mmap con la bandera MAP_HUGETLB. Los mismos 50 MB ahora abarcan 25 páginas, dentro del TLB. Esto requiere que vm.nr_hugepages esté configurado en el arranque. En producción se reservan 10.561 páginas enormes; el trie usa 24.

Resultado: Reducción del 3-12% del reloj de pared dependiendo de la longitud de entrada. La mayor ganancia se produjo en 4.098 tokens (-12,0%), donde el tráfico de la tabla de páginas competía activamente con los datos de prueba por el ancho de banda L2. Más allá de los tokens 4K, la ganancia se reduce porque dominan los fallos L3.

Resultados finales de referencia

Todas las mediciones son de un solo subproceso, fijadas a un núcleo en un Intel Xeon Platinum 8488C, con 10 000 iteraciones después de 1000 rondas de calentamiento. En 514 fichas:

Motorp50 LatenciaInstruccionesAsignacionesAbrazando la cara (caja de tokenizadores)349 µs3.60M7,295Pieza de oración (C++)128 µs1.83M1,559Tokenizador IREE (C)112 µs2.28M1Perplejidad (final, las 3 optimizaciones)~63 µs1.04M0

En toda la secuencia de optimización, las instrucciones por codificación cayeron de 3,66 millones a 1,04 millones, una reducción de 3,5 veces. El reloj de pared iguala esa relación en entradas cortas y se amplía en entradas largas donde las asignaciones por token de la referencia desbordan L2 y L3.

Un hallazgo adicional: las cajas de envoltura Rust disponibles en el mercado alrededor de SentencePiece e IREE agregan una sobrecarga de latencia de 1,6 a 1,9 veces en comparación con los binarios nativos de C/C++. La caja de piezas de oración asigna una lista nueva de piezas simbólicas en cada llamada. Los gastos generales son mensurables pero se amortizan con insumos prolongados.

El codificador final de Perplexity produce una salida exacta del token frente a la referencia. En producción, utiliza rayón para paralelizar los núcleos.

Explicador visual de Marktechpost

Lanzamiento de código abierto

Perplexity AI reescribe su tokenizador Unigram y reduce la utilización de la CPU entre 5 y 6 veces

Perplexity reimplementó su tokenizador Unigram desde cero en Rust y lo abrió en pplx-garden. Tres optimizaciones específicas eliminaron el trabajo desperdiciado del camino activo.

5 cajas de tokenizadores p50 inferiores frente a HuggingFace

Reducción de 5 a 6 veces la utilización de CPU en producción

0 asignaciones de montón en la ruta activa

Fuente: research.perplexity.ai

El problema

Por qué la tokenización de la CPU se convirtió en un cuello de botella

El costo de inferencia de LLM generalmente se enmarca en el trabajo de GPU: cachés KV, núcleos de atención, enrutamiento experto. Pero los modelos pequeños cuentan una historia diferente.

1

Los rerankers e incrustadores son pequeños.

De dos a tres órdenes de magnitud más pequeños que los transformadores de frontera. El cálculo de la GPU finaliza en milisegundos de un solo dígito.

2

La tokenización se ejecuta en la CPU antes de cada llamada.

Cada entrada pasa primero por la tokenización del lado de la CPU, convirtiendo el texto en ID de vocabulario.

3

El tamaño del lote amplifica el costo

Un reclasificador que puntúa cientos de documentos por solicitud significa que la tokenización se ejecuta cientos de veces por consulta.

Fondo

¿Qué es la tokenización de Unigram?

Introducido por Kudo (2018), implementado en SentencePieza. Perplexity apunta a XLM-RoBERTa con un vocabulario Unigram de 250.000 tokens.

Problema de ruta más probable

Cada ficha de vocabulario conlleva una probabilidad logarítmica aprendida. El tokenizador elige la segmentación cuyas puntuaciones de token suman las más altas.

Algoritmo de Viterbi (1967)

Un método de programación dinámica que encuentra el mejor camino. Las posiciones de los bytes son capas de gráficos; Las fichas de vocabulario son aristas.

La ruta activa es el recorrido de prueba interno en cada posición de byte. En una entrada de token de 16K, esto ejecuta cientos de miles de transiciones de prueba y retira decenas de millones de instrucciones por codificación.

Causa principal

Tres cuellos de botella en la referencia de la cara que abraza

Medido en 514 tokens (512 + BOS/EOS) en Intel Xeon Platinum 8488C:

BottleneckMechanismImpact Asignación por coincidenciaString::from_utf8 + AHashMap búsqueda por coincidencia trie7,295 asignaciones en 514 tokens; 299,171 a 16K Persecución de puntero por byteAHashMap en cada nodo trie; 4 cargas dependientes por paso La latencia de carga dependiente domina la paliza L2 Tabla DP y buffers de salida recién asignados en cada llamada Tasa de fallos L2: 8 % con 128 tokens, 50 % con 16 K

La asignación por token es constante: ~2 KB y ~18 asignaciones por token independientemente del tamaño de entrada.

Paso 0: línea de base

Puerto de asignación cero antes de cambiar el Trie

Antes de tocar la estructura trie, Perplexity aisló cuánto costo provino únicamente de asignaciones innecesarias. Mantuvieron el mismo intento de HashMap pero hicieron dos cambios:

Estructura temporal propiedad de la persona que llama reutilizada en todas las llamadas, eliminando la asignación de tabla DP por codificación. ID de token almacenados directamente en nodos trie, eliminando la asignación de cadenas por coincidencia y la búsqueda de mapa hash secundario.

Línea de base p50

155 µs (-2,1x)

Las asignaciones por sí solas fueron el costo dominante. Las instrucciones retiradas cayeron 2,4x. La persecución del puntero HashMap era ahora el cuello de botella restante.

Optimización 1

Trie de doble matriz

El intento de HashMap cuesta 4 cargas dependientes por paso de byte. El trie de doble matriz (Aoe, 1989) lo reemplaza con matrices de enteros planos base y check.

Prueba HashMap (referencia)

Hash byte, cargar depósito, seguir el puntero al niño, seguir el puntero al HashMap del niño. 4 cargas dependientes por paso.

Trie de doble matriz

siguiente = base[nodo] + byte
Verificar cheque[siguiente] == nodo
2 lecturas de matriz, 1 suma, 1 comparación. Sin hash.

250 000 vocabulario caben en ~9 MB de memoria contigua. El conjunto de trabajo en caliente por codificación es de ~100 KB y cabe en la caché L2. Resultado: p50 cae de 155 µs a 68 µs, un reloj de pared 4,8 veces más rápido que la referencia original.

Optimización 2

Mapa de bits + empaquetado de línea de caché de 64 bytes

El intento de doble matriz todavía necesita dos cargas de matriz dependientes por paso. Perplexity reemplazó la matriz de verificación con un mapa de bits por nodo.

Mapa de bits por nodo: cuatro palabras de 64 bits (32 bytes), un bit por valor de byte posible. Una prueba de un solo bit reemplaza la segunda carga de la matriz. Los cuatro campos por nodo (mapa de bits, base, ID de token, puntuación) empaquetados en una línea de caché de 64 bytes. Un paso de prueba ahora carga una única línea de caché que cubre la validez, el desplazamiento secundario y los datos del terminal.

Accesos L2 a 514 tokens

4.600 (dardos) frente a 1.800 (mapa de bits)

Compensación de tamaño de prueba

~9 MB (Darts) aumenta a ~50 MB (780 000 nodos x 64 bytes)

Optimización 3

2 MB de páginas enormes para Trie

Con 50 MB y 4 KB de páginas, el trie abarca aproximadamente 12 000 páginas virtuales. Intel Sapphire Rapids tiene solo 96 entradas en el TLB de datos de primer nivel. TLB no activa los recorridos de la tabla de páginas.

~9000 ciclos gastados en recorridos por tablas de páginas por codificación de 512 tokens, aproximadamente el 3 % del presupuesto por codificación.

Solución: respalde el trie con páginas grandes de 2 MB a través de mmap con MAP_HUGETLB. Los mismos 50 MB abarcan 25 páginas, dentro de la capacidad de TLB. En producción se reservan 10.561 páginas enormes; el trie usa 24.

En 514 fichas

65,4 µs sin páginas enormes frente a 63,1 µs con (-3,4%)

A 4.098 tokens

773 µs sin páginas enormes frente a 679 µs con (-12,0%)

Resultados

Punto de referencia final en 514 tokens

Núcleo fijado y de un solo subproceso, Intel Xeon Platinum 8488C. 10.000 iteraciones después de 1.000 rondas de calentamiento.

Motorp50 LatenciaInstruccionesAsignaciones Abrazando la cara (óxido)349 µs3.60M7,295 FrasePieza (C++)128 µs1.83M1,559 Tokenizador IREE (C)112 µs2.28M1 Perplejidad (final)~63 µs1.04M0

Las instrucciones por codificación cayeron de 3,66 millones a 1,04 millones, una reducción de 3,5 veces. Nota: las cajas contenedoras Rust disponibles en el mercado alrededor de SentencePiece e IREE agregan una sobrecarga de 1,6 a 1,9 veces en comparación con los binarios nativos debido a las asignaciones por llamada.

Conclusiones clave

Lo que los ingenieros deben saber

La tokenización de CPU es invisible en los rastros de perfiles de GPU, pero real en la latencia de extremo a extremo para modelos pequeños. La eliminación de las asignaciones de montón por codificación (línea de base de asignación cero) redujo p50 de 326 µs a 155 µs antes de cualquier cambio de prueba. El trie de doble matriz llevó p50 a 68 µs. El empaquetado de mapas de bits y las páginas enormes lo llevaron a ~63 µs. Las cajas contenedoras de Rust alrededor de SentencePiece e IREE agregan una sobrecarga de latencia de 1,6 a 1,9 veces en comparación con los binarios nativos. El código fuente está disponible en github.com/perplexityai/pplx-garden bajo licencia MIT.

Impacto en la producción

Reducción de utilización de CPU de 5 a 6 veces + ms de dos dígitos de latencia de reordenación

modelo objetivo

XLM-RoBERTa, vocabulario Unigram de SentencePieza de 250.000 tokens

Conclusiones clave

Perplexity reconstruyó su tokenizador Unigram apuntando al vocabulario SentencePiece de 250 000 tokens de XLM-RoBERTa. El nuevo codificador logra cero asignaciones de montón en estado estable y ~63 µs p50 en 514 tokens. Tres optimizaciones: trie de doble matriz, mapa de bits + empaquetamiento de línea de caché de 64 bytes y páginas grandes de 2 MB para el trie. Resultado intermedio: un puerto HashMap de asignación cero solo cortó p50 de 326 µs a 155 µs antes de que se cambiara el intento Impacto en la producción: reducción de 5 a 6 veces en la utilización de la CPU y reducción de ms de dos dígitos en la latencia del reranker

Consulte el repositorio y los detalles técnicos. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 150.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.

¿Necesita asociarse con nosotros para promocionar su repositorio de GitHub O su página principal de Hugging O su lanzamiento de producto O seminario web, etc.? Conéctate con nosotros