Show HN: Posible mejora de 2x en la velocidad de inferencia de LLM (Speeding up LLM inference 2x times (possibly))
(asciinema.org)¿Qué es el effort?
- El effort se refiere a la energía, el tiempo y los recursos que se invierten para lograr una tarea o alcanzar una meta
- Significa el trabajo y la dedicación necesarios para superar obstáculos o conseguir el resultado deseado
- El effort puede ser físico o mental
- esfuerzo físico, como la energía usada para levantar objetos pesados o correr un maratón
- esfuerzo mental, como la concentración y la fortaleza mental necesarias para resolver problemas complejos o aprender nuevas habilidades
- Es un elemento clave para lograr el éxito en distintas áreas de la vida, como el crecimiento personal, el trabajo y la educación
Opinión de GN⁺
- El effort no es solo la cantidad de tiempo o energía que se invierte, sino un concepto en el que también intervienen de forma compleja factores como la constancia, la concentración y la motivación. Por eso, esforzarse mucho no garantiza necesariamente el éxito
- Especialmente en trabajos que requieren effort mental, hay que tener cuidado de no caer en burnout. Es importante encontrar formas de sostener el effort mediante descansos y recompensas adecuadas
- La forma de aplicar effort puede variar según la persona. Algunas pueden concentrarlo intensamente en periodos cortos, mientras que otras pueden mantenerlo de manera constante a largo plazo. Lo más efectivo es encontrar el método que mejor se adapte a uno mismo
1 comentarios
Opiniones de Hacker News
El núcleo del algoritmo parece ser podar parámetros durante la ejecución y, dentro de cada grupo, determinar los pesos menos importantes según el orden por valor absoluto de los pesos candidatos a poda, ponerlos en cero y así volver dispersa la matriz de pesos.
Si buscas poda de modelos, aparecen muchos resultados, y https://arxiv.org/abs/2305.11627 también trata la “poda basada en magnitud” como línea base y cita https://arxiv.org/pdf/2301.00774.pdf.
No me gusta cuando los papers implementan de forma floja la línea base para presumir su propio método, o lo envuelven todo con un montón de términos matemáticos. El post del blog original explica el método de una forma mucho más fácil incluso para alguien con muy poco conocimiento previo.
Los enfoques de los papers mencionados parecen ser estáticos, y no parece que propongan un algoritmo que use resultados del 20 al 50% para acelerar realmente el cómputo. Esa fue una parte importante de lo difícil. Algún día me daré el tiempo de revisar bien la bibliografía relacionada.
Al final, quiero agregar una página de citas con los papers que la gente publicó en los comentarios. Creo que pronto alguien podría encontrar un texto donde este algoritmo ya esté descrito.
Durante el desarrollo también le pregunté a gpt-4 y busqué en Google, pero lo que encontré en general era estático o iba por el lado de eliminar arbitrariamente dimensiones/capas completas y luego reentrenar. No encontré nada que coincidiera exactamente con esta idea.
Cuanto más denso y difícil de entender es un texto, más probable es que esconda ciencia deficiente dentro.
Me gustó esta frase de la parte sobre la implementación en GPU:
“Un lector que es nuevo en programación de GPU ahora puede preguntarse: ¿cómo funciona esto?
Un lector con experiencia en programación de GPU puede preguntarse: ¿cómo diablos funciona esto?”
Me gustaría que alguien que realmente sepa Metal revise este código. Esta fue mi primera incursión en programación de GPU.
“Volteemos la matriz, ordenemos los elementos por fila y miremos la multiplicación otra vez desde esa dirección. La gente inteligente llama a esto formato de filas dispersas comprimidas (CSR). Ahora, para multiplicar, tomamos el 1 del vector, lo multiplicamos por 256 y lo sumamos a la tercera fila del vector de salida, y así sucesivamente. Ahora veamos qué pasa si recortamos la última columna, donde están los valores más bajos.”
Me da curiosidad cómo encaja CSR con la forma de reducir la cantidad de multiplicaciones.
Este enfoque se parece a la dispersidad semiestructurada, la llamada dispersidad 2:4, así que valdría la pena compararlo explícitamente. Por lo que vi al pasar, esta técnica está optimizada para Apple Silicon, da alrededor de 2x de velocidad con 75% de dispersidad, se aplica dinámicamente en tiempo de ejecución según la entrada y permite elegir el grado de dispersidad.
En cambio, la dispersidad semiestructurada 2:4 está optimizada para GPU con tensor cores dispersos, es decir, Nvidia Ampere en adelante; da alrededor de 2x de velocidad con 50% de dispersidad, es estática y se aplica sobre el estado almacenado del modelo, y con 50% de dispersidad probablemente tenga peores resultados que esta técnica.
La comparación que quisiera ver es entre los resultados de 50% de dispersidad y 2x de mejora de velocidad de la dispersidad semiestructurada, y los de 75% de dispersidad y 2x de mejora de velocidad de esta técnica.
Elegí Apple Silicon porque era fácil de desarrollar. Es posible que este algoritmo también logre buen rendimiento en otras arquitecturas.
Habiendo usado CSR, no me sorprende. Entre los formatos más recientes, puede haber algunos que se adapten mejor a las características del hardware, como block ELL.
Esos formatos evitan lecturas no coalescentes o gather, pero el código se vuelve más complicado.
bucketMul casi no tiene lecturas no coalescentes y usa una estructura de datos distinta de la CSR común. Está explicado aquí: https://kolinko.github.io/effort/bucketmul.html
Cada fila de la matriz se divide en 16 partes, y se elige qué partes leer. Las escrituras son totalmente lineales.
Aunque no estoy muy seguro de si lo que digo tiene sentido ahora. Ya es algo tarde y fue un día largo.
Buena idea y buen texto. Yo también trabajo en el área de sparsity en inferencia de redes neuronales, y se me ocurren algunas cosas que conviene tener en cuenta.
Comparado con una implementación densa de multiplicación matriz-vector, este algoritmo agrega complejidad algorítmica, pero reduce el tráfico de memoria. La multiplicación matriz-vector suele estar limitada por memoria, así que reducir los accesos a memoria aumenta el throughput. Sin embargo, cuando el tamaño de batch supera 1, es muy probable que el acceso a memoria deje de ser el cuello de botella y que la mejora de velocidad desaparezca muy rápido.
Como comparación, me gustaría ver no solo el mismo modelo, sino también otros modelos con una arquitectura 2 veces más rápida. Por ejemplo, me pregunto cómo sería aplicar este método con 50% de sparsity a un LLM de 13B parámetros frente a un LLM de 7B parámetros, o frente al mismo LLM cuantizado a la mitad del ancho de bits de referencia. Si en el mismo tiempo puede producir salidas de mayor fidelidad que los frameworks de inferencia existentes, creo que podría ser material para un paper interesante.
Como omite multiplicaciones, es probable que el error de aproximación esté sesgado a que el valor absoluto sea siempre menor que el resultado real. Si se pudiera sumar un término de corrección que compense ese error sistemático, creo que el rendimiento mejoraría un poco más.
La notación Big O no es muy adecuada para trabajo en GPU, pero en este caso es más o menos correcta.
El principal problema son los límites arquitectónicos de la GPU. Este algoritmo necesita más registros/grupos de hilos/memoria caché que el enfoque tradicional, y eso se vuelve el cuello de botella principal. Además, como todas las multiplicaciones usan buckets distintos, no es sencillo paralelizar el trabajo como en los modelos MoE.
En arquitecturas más grandes, de hecho hice muchas pruebas con Mixtral, que en la práctica es un modelo de 13B, y mi impresión es que ahí aguanta mucho mejor. La velocidad de inferencia respecto del effort se mantiene, y la calidad respecto del effort conserva resultados legibles hasta 12–16%, no 20–25%. Las pruebas fueron limitadas, y al meter la implementación de Mistral rompí la de Mixtral, así que no tengo datos firmes, pero pienso arreglarlo pronto.
Intuitivamente, creo que cuanto más grande es el modelo, más se puede recortar el effort.
Al principio yo también supuse que omitir multiplicaciones introduciría un sesgo, pero, contra la intuición, no fue así. Tengo algunos gráficos, pero todavía no están listos para publicarse.
Como los valores de la matriz están distribuidos de forma pareja entre positivos y negativos, después de cierto umbral no hay mucho drift en los valores resultantes.
Se ve excelente. Aunque la latencia de 15 ms es parecida a los 16.7 ms de la sincronización vertical a 60 Hz.
Si estás actualizando la pantalla con cada token, podría ser la causa de alguna sincronización en algún punto.
De todos modos, gracias por la idea. La voy a tomar como la primera contribución de la comunidad :D
Es una contribución realmente genial y abierta. Voy a estar atento para ver si llama.cpp implementa esto.
Estaba buscando una forma de acelerar la inferencia en CPU, y me encanta la idea de effort.
El nombre lo sugirió chatgpt. Como dice que no reconoce este enfoque, quizá realmente sea algo nuevo.
Me gustaría contactar a llama.cpp y a otros proyectos, y ojalá se implemente. También pensé en escribir yo mismo un parche para llama, pero C++ y el tamaño de ese proyecto me quedaron grandes.
La inferencia en CPU también debería acelerarse de la misma manera. Además, como se puede cargar solo una parte de los pesos —por ejemplo, saltarse los menos importantes y cargar solo el 70%—, debería ser posible ejecutar modelos con menos VRAM que antes. Eso sí, todavía hace falta una implementación Q8.
Curiosamente, cuando intenté comparar benchmarks con llama.cpp, no pude encontrar la velocidad de 7B/FP16 en una MB Air de 16 GB. Es porque con el método normal no se puede ejecutar. Con Effort sí se puede.
De forma similar, en una M2 de 96 GB ejecuté Mixtral a resolución completa pero recortado. Normalmente requiere 114 GB de RAM, pero al cargar solo el 75% de los pesos funcionó de manera fluida. Ahora la implementación quedó algo rota y genera basura, así que tengo que corregirla.
Buen artículo. Me da mucha curiosidad saber cómo es el rendimiento por VRAM comparado con la cuantización simple.
También me pregunto si hay planes de implementar una versión multiplataforma.
Claro que también se podrían cargar menos pesos, pero parece que si se quita más de 20–30% de los pesos, la calidad se degrada rápidamente.
En otras palabras, este algoritmo separa el tiempo de inferencia del uso de VRAM.
Aun así, también me da curiosidad saber si, usando effort, un Q8 recortado al 75% puede dar mejores resultados que Q6.
Pero creo que todavía faltan algunas semanas para pulir lo suficiente la implementación y probarlo bien.
Este enfoque no reentrena, pero me pregunto si usar algo así junto con cuantización y luego hacer entrenamiento adicional podría recuperar parte de la calidad perdida.
Es bueno ver cosas como esta, y también es bueno poder imaginar cuánto podrían mejorar el rendimiento y los costos en el futuro. Gracias por desarrollarlo como open source.
Una es el entrenamiento posterior que se hace después, y la otra es el entrenamiento consciente de cuantización, donde se cuantiza durante el entrenamiento pero se mantienen las activaciones y los gradientes en precisión completa.