- llama.cpp PR #7154, GGML’in CPU için SiLU ve SoftMax hesaplamasını llamafile’ın vektörleştirilmiş
expf() tabanlı uygulamasıyla yeniden yazarak 17 Mayıs 2024’te master dalına birleştirildi
- Eski GGML hız için
short[65536] lookup table kullanıyordu; yeni uygulama ise aarch64 ve SSE2+ üzerinde en kötü yuvarlama hatasını 2 ULP düzeyinde tutarken daha doğru hesaplama hedefliyor
SOFT_MAX CPU performans testinde SSE2+FMA 1,5 kat, AVX2+FMA 1,9 kat, AVX512 ise 2,1 kat hızlandı; AMD Ryzen 9 5950X ve M2 Ultra’da da master sürümüne göre yaklaşık 1,5 kat daha hızlı sonuç doğrulandı
- Değişiklikler arasında
ggml_v_expf() ve ggml_v_silu() eklenmesi, tekrar eden kodun ggml_vec_soft_max_f32() içine çıkarılması, GGML_SILU_FP16 ile ilgili fonksiyonların kaldırılması ve SSE2 ya da ARM NEON için koşullu SiLU yolunun ayarlanması yer alıyor
- Birleştirme sonrasında
>1 slots ile sunucu çalıştırıldığında deterministik olmayan sonuçlar yeniden üretildi; sonrasında nedenin -ffinite-math-only olduğu daraltıldı ve -fno-finite-math-only gerektiren bir derleme kısıtına gidildi
PR’nin değişiklik hedefi ve birleştirme durumu
- PR #7154,
ggml : rewrite silu and softmax for cpu başlığıyla, llama.cpp’nin GGML CPU yolundaki SiLU ve SoftMax hesaplamasını yeniden yazıyor
- Değişiklik, llamafile’daki vektörleştirilmiş
expf() fonksiyonunun upstream’e alınması şeklinde başladı
- PR, 17 Mayıs 2024’te
ggml-org:master içine birleştirildi ve birleştirme commit’i 934266c olarak görünüyor
- Yazara göre yeni yaklaşım, eski GGML’in hız için kullandığı
short[65536] lookup table’dan daha doğru SoftMax ve SiLU hesaplaması yapabiliyor
Doğruluk ve destek kapsamı
- Yeni
expf() tabanlı yol aarch64 ve SSE2+ destekliyor ve en kötü yuvarlama hatası 2 ULP olarak veriliyor
- İlk açıklamada AVX2 ve AVX512 uygulamalarının da yazıldığı, ancak SSE2+FMA’e kıyasla kod karmaşıklığını haklı çıkaracak kadar büyük bir fayda sağlamadıkları için dahil edilmediği belirtiliyor
- Daha sonra benchmark sonuçlarına dayanarak AVX2 ve AVX512 kodu da dahil edildi
- Ayrı bir test çıktısında
4294967296 numbers tested successfully ifadesi verildi ve çeşitli giriş değerleri için exp ile llamafile uygulamasının sonuç karşılaştırması paylaşıldı
Kod değişikliği kapsamı
- Bir inceleyicinin özetlediği başlıca değişiklikler şunlar:
- Yorum satırına alınmış
#define kaldırıldı
- Tekrarlanan 5 satır
ggml_vec_soft_max_f32() içine çıkarıldı
GGML_SILU_FP16 ile ilgili çeşitli fonksiyonlar kaldırıldı
ggml_v_expf() eklendi
ggml_v_silu() eklendi
ggml_vec_silu_f32() için, SSE2 veya __ARM_NEON bayrağına göre farklı fonksiyon kullanacak şekilde ön işleyici koşulları düzenlendi
- GitHub metaverisine göre değişen dosya sayısı 1 olarak görünüyor
- PR üzerinde
refactoring ve Review Complexity : High etiketleri bulunuyor; ikincisi, LLM veya GPU hakkında derin bilgi gerektirebileceğini belirten bir açıklama içeriyor
Benchmark ve performans sonuçları
ggerganov, AMD Ryzen 9 5950X ve M2 Ultra üzerinde SOFT_MAX işleminin master sürümünden yaklaşık 1,5 kat daha hızlı olduğunu doğruladı
- Kullanılan test komutu şu şekilde:
make -j tests && ./tests/test-backend-ops -o SOFT_MAX -b CPU perf
- Daha sonra yazar, aynı komutta performans kazanımının şu seviyelere çıktığını belirtti:
- SSE2+FMA: 1,5 kat
- AVX2+FMA: 1,9 kat
- AVX512: 2,1 kat
- Ayrı bir geliştirme betiğinde şu değerler paylaşıldı:
run_expf(): 2.98601 ns
run_llamafile_expf_sse2(): 1.35154 ns
run_llamafile_expf_avx2(): 1.16659 ns
run_llamafile_expf_avx512(): 1.18844 ns
- GitHub Actions içindeki
llama.cpp server benchmark’ı, Standard_NC4as_T4_v3 üzerinde phi-2 q4_0 yapılandırmasıyla 543 iterations kaydetti
- Eşzamanlı kullanıcı: 8
- duration: 10 dakika
- HTTP istek ortalaması: 8626.19ms
- p95: 21696.44ms
- Prompt processing ortalaması: 94.59 tk/s
- Token generation ortalaması: 33.43 tk/s
AVX512 optimizasyonu tartışması
chriselrod, AVX512 üzerinde vscalefps kullanılmasını önerdi
vscalefps, zmm0 = zmm1 * 2^{zmm2} hesabını yapıyor
- Overflow ve underflow durumlarını uygun şekilde işleyerek kontrolleri ve blend işlemlerini kaldırabileceği belirtildi
- Julia uygulama örneği ve assembly döngüsü paylaşıldı; testler doğruysa
x=47.483456f için maksimum hata 1 ULP’nin altında oldu
vscalefps yaklaşımının lookup table kullanmadığı, Float64/double uygulamasında ise vpermi2pd üzerinden 16 elemanlı bir lookup table kullanıldığı açıklandı
- Daha sonra bir C++ uygulama bağlantısı da paylaşıldı
- ExpAVX512
- Kaynak kod
include/ExpAVX512.hpp içinde bulunuyor
- README içinde benchmark’lar yer alıyor, ancak diğer uygulamalarla karşılaştırmalı benchmark yapılmadığı belirtiliyor
Birleştirme sonrası deterministik olmama sorunu
- Birleştirme sonrasında sunucuda
>1 slots kullanıldığında deterministik olmayan sonuçlar üretildiğine dair yeniden üretilebilir bir örnek bildirildi
- En küçük yeniden üretim adımları şöyle:
make clean && make server
./server -m models/opt/llama_2-7b-q4_0.gguf --parallel 2 --threads 1
- Başka bir kabukta çalıştırılan istek ise şöyle:
curl --request POST --url http://localhost:8080/completion --header "Content-Type: application/json" --data '{"prompt": "", "n_predict":10, "n_probs": 2, "temperature": -1}' | python3 -m json.tool
- Son token’ın token olasılıklarının her
curl çağrısında iki değer arasında döndüğü, 4 slot kullanıldığında ise dört olası değer arasında döndüğü belirtildi
-ffinite-math-only ve derleme kısıtı
- Daha sonra ilgili commit’lerde, sorunun nedeninin
-ffinite-math-only olduğuna daraltıldığına atıf yapıldı
- Bu sorunun, SiLU’nun küçük değerleri 0’a flush etmek yerine NaN ya da başka bozuk değerler döndürmesinden kaynaklandığının düşünüldüğü kaydedildi
- Düzeltme olarak
-fno-finite-math-only ayarının etkin olup olmadığı kontrol edildi ve derleme modunun finite math modu olmamasını zorunlu kılan bir denetim eklendi
- Hata mesajı, GGML’in bazı rutinlerinin non-finite math arithmetic gerektirdiğini ve derleyiciye
-fno-finite-math-only verilmesi gerektiğini söylüyor
- Sonrasında kullanıcılar,
-Ofast veya -ffast-math seçeneklerinin -ffinite-math-only içererek derlemeyi bozabildiği deneyimlerini paylaştı
- GCC 13.2’ye kadar
-Ofast kullanılabilirken, GCC 14’ten itibaren sonuçların bozuk hale geldiğine dair bir bildirim var
- Bazı testlerde
-fno-finite-math-only yanında -fmath-errno da gerektiği belirtildi
- Çeşitli depolardaki takip commit’lerinde,
-ffast-math kaldırılarak ya da -fno-finite-math-only açıkça eklenerek ggml derleme hatasının giderildiğine atıf yapıldı
1 yorum
Hacker News yorumları
Yaklaşık 20 yıl önce Hughes radar sinyal işlemcisi için program yazarken 0 < x < 1 aralığında e^x hesaplamam gerekiyordu
O işlemcide çarpma vardı, bu yüzden 32 bitlik sözcüğün 4 adet 8 bitlik bloğunun her biri için olası 256 değerden oluşan 4 tane e^x tablosu oluşturup bunları çarparak nihai değeri elde etmiştim
Önceki en iyi e^x rutininden yaklaşık 5 kat daha hızlıydı ve bugün eski moda kalsa da bir süre boyunca kağıt üzerinde çok daha hızlı olan işlemcilerden daha hızlı radar sinyali işleyen eğlenceli bir makineydi
Teknik olarak a,
high byte << 24gibi bir biçimde olduğundan e^a tablosu a => e^(a<<24) eşlemesi oluyor; diğer baytlar da benzer şekilde ele alınıyorBu tür silu ve softmax iyileştirmelerinin genel LLM çıkarım hızına ne kadar etki ettiğini merak ediyorum
Yanlışsam düzeltin ama zamanın çoğu matris çarpımına gidiyorsa bu değişikliğin etkisi küçük gibi görünüyor
Biraz konu dışı ama hızlıca bakarken “bu bayağı çılgın bir optimizasyon gibi. Karmaşık ve zaten birçok kişinin baktığı bir kod” diye düşündüm, sonra katkıda bulunanı görünce “tabii ki jart. Çılgın derecede iyi çözümler hep jart’tan çıkıyor” dedim
O dünyadaki birçok şey gibi bu acının da epey kısmı kendi kendine yaratılmış
Bildiğim kadarıyla C# tarzı SIMD ve donanım intrinsic sözdizimini mümkün kılan C++ kütüphaneleri de var, ama komut kümesi dokümantasyonunda mnemonic’leri doğrudan aramayı zorlaştırma gibi bir dezavantajları oluyor
Burada yapılan işin önemini küçümsemeye çalışmıyorum; sadece daha geniş bir okur kitlesi için daha erişilebilir olabilirdi diyorum. Yine de hâlâ çıkarım backend’ini C# ile baştan yazmayı önermek gibi, burada herkesin absürt bulacağı bir öneri yapmıyorum
adapted from arm limited optimized routineyani sonuçta devlerin omuzlarında duruluyor“Herkesin görmezden geldiği o sabit, mühendislikte kafanızı bütünüyle yiyip bitirebilir” diyen ünlü bir hocayı hatırlatıyor
short[65536]lookup table yerine geçiyor deniyor; baştan biraz kaba bir seçim değil mi diye düşündümBu neredeyse L1 önbelleğinin tamamı büyüklüğünde bir lookup table; olasılıksal olarak kabaca işe yaradığı için mi beklenmedik biçimde iyi çalışıyor?
L1 önbelleğini boşaltmanın pek önemi yok ve LUT yüzünden dışarı itilen veriler zaten neredeyse hiç yeniden kullanılmayacaktı
Makine öğrenimi yükleri genelde her yinelemede tüm veri kümesini doğrusal okuyan streaming load türünde işlerdir
Kendi sınırlı deneyimime göre, lookup table’dan hızlı hale gelmeden önce gerçek zamanlı hesaplamayla oldukça fazla şey yapılabiliyor
llama.cpp’de, konu CPU için
Şimdi bunu llama.cpp projesine upstream etmeye çalışıyoruz
Şu an yalnızca llamafile’da bulunan başka performans iyileştirmeleri de var; örneğin Kawrakow’un K quants’ı çok daha hızlı hâle getiren çalışması
Biraz konu dışı olabilir ama ggml gibi bir şeyin tensorflow lite, onnxruntime gibi çalışma zamanlarıyla karşılaştırıldığında nasıl olduğunu bilen var mı?
Kısaca, LLM için doğru tercih llama.cpp; temel bağımlılığı olan GGML ile whisper da çalıştırabilirsiniz
Onun dışındaki şeyler için ONNX kullanın
TF, makine öğrenimi dünyasının Apple’ı gibi; Google ML ekosistemine tamamen bağlıysanız harika, ama onun dışında fiilen ölü sayılır. HF modellerinin saçma derecede büyük bir oranı, yaklaşık %94’ü, PyTorch
Doğrudan çıkarım performansı kıyası açısından gerçekten denemeye değer olan muhtemelen ONNX üzerindeki Whisper ile GGML olur; birisi llama.cpp kütüphanemi Whisper ile çalıştırdı ve anlamlı bir performans farkı bildirmedi
Şu anda CUDA cihazlarda batch’siz çıkarım için gguf/llama.cpp daha performanslı çözüm mü, yoksa exllamav2+flashattention hâlâ önde mi?
4 bit KV cache gibi daha önemli farklar var
LUT de vektörleştirilebilir
https://www.intel.com/content/www/us/en/docs/intrinsics-guid...
Bir zamanlar LUT ile yapılabilecek şeyler hakkında da yazmıştım https://darkcephas.blogspot.com/2018/10/validating-utf8-stri...
exp’yi doğrudan uygulasanız bile istenen doğruluğa bağlı olarak 10-20 FMA civarı yeterli oluyorgather ya da permutation’ın saf hesaplamayla yarışması zor
Benzer şekilde daha hızlı bir tanh da var https://github.com/microsoft/onnxruntime/pull/20612
Ama hedef ne acaba? O GeLU yaklaşımını daha da hızlandırmak mı?
erff()kullanımına geri dönülürse muhtemelen çok daha hızlı olur gibi geliyorBu, gguf’un kısmi GPU offload kullanım durumuna da yardımcı oluyor mu?
CPU tarafı da daha mı hızlı oluyor?