1 puan yazan GN⁺ 2024-05-16 | 1 yorum | WhatsApp'ta paylaş
  • 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

 
GN⁺ 2024-05-16
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

    • Takip etmesi zorsa fikir kabaca şu gibi görünüyor: e^x = e^(a+b+c+d), burada a/b/c/d x'in her bir baytı; bunu e^a * e^b * e^c * e^d olarak yeniden yazıp her biri için e^a, e^b lookup table oluşturuyorsunuz
      Teknik olarak a, high byte << 24 gibi bir biçimde olduğundan e^a tablosu a => e^(a<<24) eşlemesi oluyor; diğer baytlar da benzer şekilde ele alınıyor
  • Bu 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

    • Kayan nokta işlemlerinin çoğunun matris çarpımı için harcandığı doğru, ancak softmax orantısız biçimde çok bellek bant genişliği kullandığı için, yalnızca işlem miktarına bakarak tahmin edeceğinizden genelde çok daha uzun sürü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

    • Esas olarak korkutucu görünmesinin sebebi C/C++ intrinsics sözdiziminin zaten böyle olması
      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 routine yani sonuçta devlerin omuzlarında duruluyor
    • Bunun asimptotik analiz derslerinde öğretilen bir şey olmadığını düşünüyorum
      “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üm
    Bu 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?

    • lookup table’ın şaşırtıcı derecede iyi çalışmasının nedeni iş yükünün zaten aşırı önbellek dostu olmaması
      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
    • lookup table’ların neden çoğu zaman kullanılmaması gerektiğini anlatan şu yazı https://specbranch.com/posts/lookup-tables/ genel olarak ne zaman uygun olduklarını ele alıyor
      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

    • Bu aslında önce llamafile için geliştirildi ve son iki sürüme dahil edildi: https://github.com/Mozilla-Ocho/llamafile/releases/tag/0.8.2
      Ş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ı?

    • ONNX ve llama.cpp Flutter kütüphanelerini 6 True Platforms’ın tamamında koruyup sürdürdüğüm için bunu oldukça iyi biliyorum
      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
    • Tam olarak hangi donanım için konuşulduğu önemli
  • Ş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?

    • 2x 4090 üzerinde fark ihmal edilebilir düzeyde
      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...

    • Doğru, ama exp’yi doğrudan uygulasanız bile istenen doğruluğa bağlı olarak 10-20 FMA civarı yeterli oluyor
      gather 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

    • Harika bir çalışma
      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 geliyor
  • Bu, gguf’un kısmi GPU offload kullanım durumuna da yardımcı oluyor mu?
    CPU tarafı da daha mı hızlı oluyor?