Mejora de las velocidades de inferencia LLM en CPU con cuantificación de modelos |  de Eduardo Álvarez |  febrero de 2024
Imagen propiedad del autor — Crear con Nightcafe

Descubra cómo mejorar significativamente la latencia de inferencia en las CPU utilizando técnicas de cuantificación para precisiones mixtas, int8 e int4.

Uno de los desafíos más importantes que enfrenta el espacio de la IA es la necesidad de recursos informáticos para alojar aplicaciones basadas en LLM de nivel de producción a gran escala. A escala, las aplicaciones LLM requieren redundancia, escalabilidad y confiabilidad, que históricamente solo han sido posibles en plataformas informáticas generales como las CPU. Aún así, la narrativa predominante hoy en día es que las CPU no pueden manejar la inferencia LLM con latencias comparables a las de las GPU de alta gama.

Una herramienta de código abierto en el ecosistema que puede ayudar a abordar los desafíos de latencia de inferencia en las CPU es Extensión Intel para PyTorch (IPEX), que proporciona optimizaciones de funciones actualizadas para un aumento adicional del rendimiento en el hardware Intel. IPEX ofrece una variedad de optimizaciones fáciles de implementar que utilizan instrucciones a nivel de hardware. Este tutorial profundizará en la teoría de la compresión de modelos y las técnicas de compresión de modelos listas para usar que proporciona IPEX. Estas técnicas de compresión impactan directamente el rendimiento de inferencia LLM en plataformas informáticas generales, como las CPU Intel de cuarta y quinta generación.

Después de la seguridad de las aplicaciones, la latencia de inferencia es uno de los parámetros más críticos de una aplicación de IA en producción. Con respecto a las aplicaciones basadas en LLM, la latencia o el rendimiento a menudo se miden en tokens/segundo. Como se ilustra en la secuencia de procesamiento de inferencia simplificada a continuación, el modelo de lenguaje procesa los tokens y luego los convierte en lenguaje natural.

GIF 1. de la secuencia de procesamiento de inferencia – Imagen del autor

Interpretar la inferencia de esta manera a veces puede llevarnos por mal camino porque analizamos este componente de las aplicaciones de IA en una abstracción del paradigma tradicional del software de producción. Sí, las aplicaciones de IA tienen sus matices, pero al fin y al cabo, todavía estamos hablando de transacciones por unidad de tiempo. Si empezamos a pensar en la inferencia como una transacción, como cualquier otra, desde el punto de vista del diseño de aplicaciones, el problema se vuelve menos complejo. Por ejemplo, digamos que tenemos una aplicación de chat que tiene los siguientes requisitos:

  • Promedio de 300 sesiones de usuario por hora
  • Promedio de 5 transacciones (solicitudes de inferencia LLM) por usuario por sesión
  • Promedio 100 fichas generado por transacción
  • Cada sesión tiene un promedio de 10.000 ms (10 s) de sobrecarga para autenticación de usuarios, barreras de seguridad, latencia de red y procesamiento previo y posterior.
  • Los usuarios toman un promedio de 30.000 ms (30 s) para responder cuando interactúa activamente con el chatbot.
  • El promedio total de activos El objetivo de tiempo de sesión es 3. minutos o menos.

A continuación, puede ver que con algunos cálculos simples, podemos obtener algunos cálculos aproximados para la latencia requerida de nuestro motor de inferencia LLM.

Figura 1. Una ecuación simple para calcular la transacción requerida y la latencia del token en función de diversos requisitos de la aplicación. — Imagen del autor

Alcanzar los umbrales de latencia requeridos en producción es un desafío, especialmente si es necesario hacerlo sin incurrir en costos adicionales de infraestructura informática. En el resto de este artículo, exploraremos una forma en la que podemos mejorar significativamente la latencia de inferencia mediante la compresión de modelos.

La compresión de modelos es un término complejo porque aborda una variedad de técnicas, como la cuantificación, destilación, poda y más de modelos. En esencia, el objetivo principal de estas técnicas es reducir la complejidad computacional de las redes neuronales.

GIF 2. Ilustración de la secuencia de procesamiento de inferencia – Imagen del autor

El método en el que nos centraremos hoy es la cuantificación de modelos, que implica reducir la precisión de bytes de los pesos y, en ocasiones, las activaciones, reduciendo la carga computacional de las operaciones matriciales y la carga de memoria de mover valores más grandes y de mayor precisión. La siguiente figura ilustra el proceso de cuantificación de los pesos de fp32 en int8.

Fig 2. Representación visual de la cuantificación del modelo que va desde la precisión total en FP32 hasta un cuarto de precisión en INT8, lo que teóricamente reduce la complejidad del modelo en un factor de 4. — Imagen del autor

Vale la pena mencionar que la reducción de la complejidad en un factor de 4 que resulta de la cuantificación de fp32 (precisión total) a int8 (precisión de un cuarto) no da como resultado una reducción de latencia de 4x durante la inferencia porque la latencia de inferencia involucra más factores además del modelo. propiedades céntricas.

Como ocurre con muchas cosas, no existe un enfoque único para todos y, en este artículo, exploraremos tres de mis técnicas favoritas para cuantificar modelos usando IPEX:

Precisión mixta (bf16/fp32)

Esta técnica cuantifica algunos, pero no todos, los pesos de la red neuronal, lo que da como resultado una compresión parcial del modelo. Esta técnica es ideal para modelos más pequeños, como los <1B LLM del mundo.

Fig 3. Ilustración simple de previsiones mixtas, que muestra los pesos FP32 en naranja y los pesos bf16 cuantificados con media precisión en verde. — Imagen del autor

La implementación es bastante sencilla: utilizando transformadores de cara abrazada, se puede cargar un modelo en la memoria y optimizarlo utilizando la función de optimización específica de IPEX llm. ipex.llm.optimize(model, dtype=dtype) configurando dtype = torch.bfloat16, Podemos activar la capacidad de inferencia de precisión mixta, que mejora la latencia de inferencia con respecto a la precisión total (fp32) y el stock.

import sys
import os
import torch
import intel_extension_for_pytorch as ipex
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline

# PART 1: Model and tokenizer loading using transformers
tokenizer = AutoTokenizer.from_pretrained("Intel/neural-chat-7b-v3-3")
model = AutoModelForCausalLM.from_pretrained("Intel/neural-chat-7b-v3-3")

# PART 2: Use IPEX to optimize the model
#dtype = torch.float # use for full precision FP32
dtype = torch.bfloat16 # use for mixed precision inference
model = ipex.llm.optimize(model, dtype=dtype)

# PART 3: Create a hugging face inference pipeline and generate results
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
st = time.time()
results = pipe("A fisherman at sea...", max_length=250)
end = time.time()
generation_latency = end-st

print('generation latency: ', generation_latency)
print(results[0]['generated_text'])

De las tres técnicas de compresión que exploraremos, esta es la más fácil de implementar (medida por líneas de código únicas) y ofrece la mejora neta más pequeña con respecto a una línea de base no cuantificada.

SmoothQuant (int8)

Esta técnica aborda los desafíos principales de la cuantificación de LLM, que incluyen el manejo de valores atípicos de gran magnitud en los canales de activación en todas las capas y tokens, un problema común que las técnicas de cuantificación tradicionales luchan por gestionar de manera efectiva. Esta técnica emplea una transformación matemática conjunta tanto en pesos como en activaciones dentro del modelo. La transformación reduce estratégicamente la disparidad entre los valores atípicos y no atípicos de las activaciones, aunque a costa de aumentar esta relación de ponderaciones. Este ajuste hace que las capas de Transformer sean “compatibles con la cuantificación”, lo que permite la aplicación exitosa de la cuantificación int8 sin degradar la calidad del modelo.

Fig 4. Ilustración simple de SmoothQuant que muestra pesos como círculos y activaciones como triángulos. El diagrama muestra los dos pasos principales: (1) la aplicación del escalador para suavizar y (2) la cuantificación a int8 – Imagen del autor

A continuación, encontrará una implementación simple de SmoothQuant, omitiendo el código para crear el DataLoader, que es un principio común y bien documentado de PyTorch. SmoothQuant es una receta de cuantificación posterior al entrenamiento que tiene en cuenta la precisión, lo que significa que al proporcionar un conjunto de datos y un modelo de calibración podrá proporcionar una línea de base y limitar la degradación del modelado del lenguaje. El modelo de calibración genera una configuración de cuantificación, que luego se pasa a ipex.llm.optimize() junto con el mapeo SmoothQuant. Tras la ejecución, se aplica SmoothQuant y el modelo se puede probar utilizando el .generate() método.

import torch
import intel_extension_for_pytorch as ipex
from intel_extension_for_pytorch.quantization import prepare
import transformers

# PART 1: Load model and tokenizer from Hugging Face + Load SmoothQuant config mapping
tokenizer = AutoTokenizer.from_pretrained("Intel/neural-chat-7b-v3-3")
model = AutoModelForCausalLM.from_pretrained("Intel/neural-chat-7b-v3-3")
qconfig = ipex.quantization.get_smooth_quant_qconfig_mapping()

# PART 2: Configure calibration
# prepare your calibration dataset samples
calib_dataset = DataLoader({Your dataloader parameters})
example_inputs = # provide a sample input from your calib_dataset
calibration_model = ipex.llm.optimize(
model.eval(),
quantization_config=qconfig,
)
prepared_model = prepare(
calibration_model.eval(), qconfig, example_inputs=example_inputs
)
with torch.no_grad():
for calib_samples in enumerate(calib_dataset):
prepared_model(calib_samples)
prepared_model.save_qconf_summary(qconf_summary=qconfig_summary_file_path)

# PART 3: Model Quantization using SmoothQuant
model = ipex.llm.optimize(
model.eval(),
quantization_config=qconfig,
qconfig_summary_file=qconfig_summary_file_path,
)

# generation inference loop
with torch.inference_mode():
model.generate({your generate parameters})

SmoothQuant es una poderosa técnica de compresión de modelos y ayuda a mejorar significativamente la latencia de inferencia en comparación con los modelos de precisión total. Aun así, se requiere un poco de trabajo inicial para preparar un conjunto de datos y un modelo de calibración.

Cuantización de solo peso (int8 e int4)

En comparación con la cuantificación int8 tradicional aplicada tanto a la activación como al peso, la cuantificación solo de peso (WOQ) ofrece un mejor equilibrio entre rendimiento y precisión. Vale la pena señalar que int4 WOQ requiere descuantificar a bf16/fp16 antes del cálculo (Figura 4), lo que introduce una sobrecarga en el cálculo. Una técnica básica de WOQ, la cuantificación asimétrica de redondeo al más cercano (RTN) por tensor, presenta desafíos y a menudo conduce a una precisión reducida (fuente). Sin embargo, la literatura (Zhewei Yao, 2022) sugiere que la cuantificación grupal de los pesos del modelo ayuda a mantener la precisión. Dado que los pesos sólo se descuantifican para el cálculo, a pesar de este paso adicional se mantiene una ventaja de memoria significativa.

Fig 5. Ilustración simple de cuantificación solo de peso, con pesos precuantizados en naranja y los pesos cuantificados en verde. Tenga en cuenta que esto representa la cuantificación inicial a int4/int8 y la descuantización a fp16/bf16 para el paso de cálculo. — Imagen del autor

La implementación de WOQ a continuación muestra las pocas líneas de código necesarias para cuantificar un modelo de Hugging Face con esta técnica. Al igual que con las implementaciones anteriores, comenzamos cargando un modelo y un tokenizador de Hugging Face. Podemos usar el get_weight_only_quant_qconfig_mapping() Método para configurar la receta WOQ. Luego la receta se pasa al ipex.llm.optimize() funcionan junto con el modelo para optimización y cuantificación. El modelo cuantificado se puede utilizar luego para inferir con el .generate() método.

import torch
import intel_extension_for_pytorch as ipex
import transformers

# PART 1: Model and tokenizer loading
tokenizer = AutoTokenizer.from_pretrained("Intel/neural-chat-7b-v3-3")
model = AutoModelForCausalLM.from_pretrained("Intel/neural-chat-7b-v3-3")

# PART 2: Preparation of quantization config
qconfig = ipex.quantization.get_weight_only_quant_qconfig_mapping(
weight_dtype=torch.qint8, # or torch.quint4x2
lowp_mode=ipex.quantization.WoqLowpMode.NONE, # or FP16, BF16, INT8
)
checkpoint = None # optionally load int4 or int8 checkpoint

# PART 3: Model optimization and quantization
model = ipex.llm.optimize(model, quantization_config=qconfig, low_precision_checkpoint=checkpoint)

# PART 4: Generation inference loop
with torch.inference_mode():
model.generate({your generate parameters})

Como puede ver, WOQ proporciona una manera poderosa de comprimir modelos a una fracción de su tamaño original con un impacto limitado en las capacidades de modelado del lenguaje.

Como ingeniero en Intel, trabajé en estrecha colaboración con el equipo de ingeniería IPEX de Intel. Esto me ha brindado una visión única de sus ventajas y hoja de ruta de desarrollo, lo que convierte a IPEX en una herramienta preferida. Sin embargo, para los desarrolladores que buscan simplicidad sin la necesidad de gestionar una dependencia adicional, PyTorch ofrece tres recetas de cuantificación: Modo Eager, Modo FX Graph (en mantenimiento) y Cuantización de exportación PyTorch 2, que brindan alternativas sólidas y menos especializadas.

Independientemente de la técnica que elija, las técnicas de compresión de modelos darán como resultado cierto grado de pérdida de rendimiento del modelado del lenguaje, aunque en <1% en muchos casos. Por este motivo, es esencial evaluar la tolerancia a fallas de la aplicación y establecer una línea de base para el rendimiento del modelo con precisión total (FP32) y/o media precisión (BF16/FP16) antes de continuar con la cuantificación.

En aplicaciones que aprovechan cierto grado de aprendizaje en contexto, como recuperación de generación aumentada (RAG), la compresión de modelos puede ser una excelente opción. En estos casos, el conocimiento de misión crítica se introduce en el modelo en el momento de la inferencia, por lo que el riesgo se reduce considerablemente incluso con aplicaciones con baja tolerancia a fallas.

La cuantificación es una excelente manera de abordar los problemas de latencia de inferencia de LLM sin actualizar ni ampliar la infraestructura informática. Vale la pena explorarlo independientemente de su caso de uso, e IPEX proporciona una buena opción para comenzar con solo unas pocas líneas de código.

Algunas cosas interesantes para probar serían:

  • Pruebe el código de muestra en este tutorial en Intel Developer Cloud’s Entorno Jupyter gratuito.
  • Tome un modelo existente que esté ejecutando en un acelerador con total precisión y pruébelo en una CPU en int4/int8
  • Explore las tres técnicas y determine cuál funciona mejor para su caso de uso. Asegúrese de comparar la pérdida de rendimiento del modelado del lenguaje, no solo la latencia.
  • Cargue su modelo cuantificado en Hugging Face Model Hub! Si es así, házmelo saber. ¡Me encantaría comprobarlo!

¡Gracias por leer! No olvides seguir mi perfil para más artículos ¡como esto!