1 poin oleh GN⁺ 2024-05-16 | 1 komentar | Bagikan ke WhatsApp
  • llama.cpp PR #7154 menulis ulang perhitungan SiLU dan SoftMax untuk CPU di GGML berdasarkan implementasi expf() tervektorisasi dari llamafile, dan digabungkan ke master pada 17 Mei 2024
  • GGML sebelumnya menggunakan lookup table short[65536] demi kecepatan, tetapi implementasi baru menargetkan perhitungan yang lebih akurat sambil mempertahankan galat pembulatan terburuk 2 ULP pada aarch64 dan SSE2+
  • Dalam uji performa CPU SOFT_MAX, SSE2+FMA menjadi 1,5x lebih cepat, AVX2+FMA 1,9x, dan AVX512 2,1x; hasil sekitar 1,5x lebih cepat dibanding master juga dikonfirmasi pada AMD Ryzen 9 5950X dan M2 Ultra
  • Perubahan mencakup penambahan ggml_v_expf() dan ggml_v_silu(), ekstraksi kode duplikat ke ggml_vec_soft_max_f32(), penghapusan fungsi terkait GGML_SILU_FP16, serta penyesuaian jalur SiLU bersyarat untuk SSE2 atau ARM NEON
  • Setelah merge, hasil nondeterministik dapat direproduksi saat server berjalan dengan >1 slots, lalu penyebabnya dipersempit ke -ffinite-math-only, yang berujung pada batasan build bahwa -fno-finite-math-only diperlukan

Tujuan perubahan PR dan status merge

  • PR #7154 berjudul ggml : rewrite silu and softmax for cpu, yang menulis ulang perhitungan SiLU dan SoftMax pada jalur CPU GGML di llama.cpp
  • Perubahan dimulai sebagai upaya meng-upstream fungsi expf() tervektorisasi dari llamafile
  • PR ini digabungkan ke ggml-org:master pada 17 Mei 2024, dan commit merge ditandai sebagai 934266c
  • Penulis menyatakan bahwa pendekatan baru dapat menghitung SoftMax dan SiLU dengan lebih akurat dibanding lookup table short[65536] yang sebelumnya digunakan GGML demi kecepatan

Akurasi dan cakupan dukungan

  • Jalur baru berbasis expf() mendukung aarch64 dan SSE2+, dengan galat pembulatan terburuk sebesar 2 ULP
  • Pada penjelasan awal, implementasi AVX2 dan AVX512 juga telah dibuat, tetapi disebut belum dimasukkan karena manfaatnya dianggap belum cukup besar dibanding kompleksitas kode relatif terhadap SSE2+FMA
  • Setelah itu, berdasarkan hasil benchmark, kode AVX2 dan AVX512 juga dimasukkan
  • Output pengujian terpisah menampilkan 4294967296 numbers tested successfully, disertai perbandingan hasil exp dan implementasi llamafile untuk berbagai nilai input

Cakupan perubahan kode

  • Perubahan utama yang dirangkum reviewer adalah sebagai berikut
    • menghapus #define yang dikomentari
    • mengekstrak 5 baris duplikat ke ggml_vec_soft_max_f32()
    • menghapus sejumlah fungsi terkait GGML_SILU_FP16
    • menambahkan ggml_v_expf()
    • menambahkan ggml_v_silu()
    • menyesuaikan preprocessor agar ggml_vec_silu_f32() menggunakan fungsi berbeda sesuai flag SSE2 atau __ARM_NEON
  • Jumlah file yang diubah ditampilkan sebagai 1 pada metadata GitHub
  • PR ini diberi label refactoring dan Review Complexity : High; label kedua disertai penjelasan bahwa perubahan ini dapat memerlukan pengetahuan mendalam tentang LLM atau GPU

Benchmark dan hasil performa

  • ggerganov mengonfirmasi bahwa SOFT_MAX pada AMD Ryzen 9 5950X dan M2 Ultra sekitar 1,5x lebih cepat daripada master
  • Perintah pengujian yang digunakan adalah sebagai berikut
make -j tests && ./tests/test-backend-ops -o SOFT_MAX -b CPU perf
  • Setelah itu, penulis menyatakan bahwa pada perintah yang sama keuntungan performanya meningkat sebagai berikut
    • SSE2+FMA: 1,5x
    • AVX2+FMA: 1,9x
    • AVX512: 2,1x
  • Dalam skrip pengembangan terpisah, angka berikut dibagikan
    • 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
  • Benchmark GitHub Actions untuk llama.cpp server mencatat 543 iterations pada Standard_NC4as_T4_v3 dengan konfigurasi phi-2 q4_0
    • pengguna bersamaan: 8
    • duration: 10 menit
    • rata-rata request HTTP: 8626.19ms
    • p95: 21696.44ms
    • rata-rata pemrosesan prompt: 94.59 tk/s
    • rata-rata generasi token: 33.43 tk/s

Diskusi optimisasi AVX512

  • chriselrod mengusulkan penggunaan vscalefps pada AVX512
    • vscalefps menghitung zmm0 = zmm1 * 2^{zmm2}
    • disebut dapat menangani overflow dan underflow dengan tepat sehingga checks dan blends bisa dihapus
  • Contoh implementasi Julia dan loop assembly dibagikan, dan jika pengujiannya benar maka galat maksimum pada x=47.483456f disebut kurang dari 1 ULP
  • Pendekatan vscalefps tidak menggunakan lookup table, dan dijelaskan bahwa implementasi Float64/double menggunakan lookup table 16 elemen melalui vpermi2pd
  • Tautan implementasi C++ juga dibagikan
    • ExpAVX512
    • sumbernya ada di include/ExpAVX512.hpp
    • README menyertakan benchmark, tetapi disebut tidak melakukan benchmark perbandingan dengan implementasi lain

Masalah nondeterminisme setelah merge

  • Setelah merge, dilaporkan ada kasus reproduksi hasil nondeterministik di server saat menggunakan >1 slots
  • Langkah reproduksi minimalnya adalah sebagai berikut
make clean && make server
./server -m models/opt/llama_2-7b-q4_0.gguf --parallel 2 --threads 1
  • Request yang dijalankan dari shell lain adalah sebagai berikut
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
  • Dikatakan bahwa probabilitas token untuk token terakhir berputar antara dua nilai pada setiap pemanggilan curl, dan saat memakai 4 slots, nilainya berputar di antara empat kemungkinan nilai

-ffinite-math-only dan batasan build

  • Commit terkait setelahnya merujuk pada hasil penyelidikan yang mempersempit penyebab masalah ke -ffinite-math-only
  • Masalah tersebut dicatat diduga terjadi karena SiLU mengembalikan NaN atau nilai garbage lain alih-alih mem-flush nilai kecil menjadi 0
  • Perbaikannya memeriksa apakah -fno-finite-math-only telah disetel, dan memaksa pemeriksaan bahwa mode kompilasi tidak berada dalam finite math mode
  • Pesan error menjelaskan bahwa beberapa rutin GGML memerlukan non-finite math arithmetic dan meminta pengguna meneruskan -fno-finite-math-only ke compiler
  • Setelah itu, pengguna lain membagikan pengalaman bahwa -Ofast atau -ffast-math dapat merusak build karena menyertakan -ffinite-math-only
    • dilaporkan bahwa hingga GCC 13.2, -Ofast masih bisa dipakai, tetapi mulai GCC 14 hasilnya menjadi garbage
    • pada beberapa pengujian, selain -fno-finite-math-only, -fmath-errno juga diperlukan
    • beberapa commit lanjutan di berbagai repositori dirujuk setelah kesalahan kompilasi ggml diselesaikan dengan menghapus -ffast-math atau secara eksplisit menambahkan -fno-finite-math-only

1 komentar

 
GN⁺ 2024-05-16
Komentar Hacker News
  • Sekitar 20 tahun lalu, saat memprogram untuk prosesor sinyal radar Hughes, saya perlu menghitung e^x untuk rentang 0 < x < 1
    Prosesor itu punya operasi perkalian, jadi saya membuat 4 tabel e^x yang masing-masing berisi 256 nilai yang mungkin untuk tiap blok 8-bit dari sebuah word 32-bit, lalu mengalikannya untuk mendapatkan nilai akhirnya
    Hasilnya sekitar 5x lebih cepat daripada rutin e^x terbaik sebelumnya, dan meski sekarang sudah kuno, itu mesin yang menyenangkan karena sempat memproses sinyal radar lebih cepat daripada prosesor-prosesor yang secara nominal jauh lebih cepat

    • Jika sulit diikuti, idenya kira-kira e^x = e^(a+b+c+d), di mana a/b/c/d adalah masing-masing byte dari x, lalu diubah menjadi e^a * e^b * e^c * e^d, kemudian dibuat lookup table e^a, e^b untuk masing-masingnya
      Secara ketat, a berbentuk seperti high byte << 24, jadi tabel e^a memetakan a => e^(a<<24), dan byte-byte lainnya diperlakukan serupa
  • Saya penasaran seberapa besar peningkatan silu dan softmax seperti ini memengaruhi keseluruhan kecepatan inferensi LLM
    Tolong koreksi kalau saya salah, tetapi karena sebagian besar waktu dipakai untuk perkalian matriks, rasanya efek perubahan ini akan kecil

    • Memang benar sebagian besar operasi floating point dipakai untuk perkalian matriks, tetapi softmax memakai bandwidth memori secara tidak proporsional besar, jadi biasanya butuh waktu jauh lebih lama daripada yang diperkirakan jika hanya melihat jumlah komputasinya
  • Sedikit menyimpang, tetapi saat membaca sekilas saya berpikir, “ini optimisasi yang cukup gila. Rumit, dan ini kode yang sudah dilihat banyak orang,” lalu setelah melihat kontributornya saya langsung berpikir, “oh, tentu saja jart. Solusi yang gila tapi bagus memang selalu dari jart”

    • Alasan utamanya terlihat menakutkan adalah karena sintaks intrinsics di C/C++ memang seperti itu
      Seperti banyak hal di ranah itu, rasa sakit ini cukup banyak yang dibuat sendiri
      Setahu saya ada juga pustaka C++ yang memungkinkan sintaks SIMD dan hardware intrinsic ala C#, tetapi kekurangannya adalah mnemonic di dokumentasi set instruksi jadi lebih sulit dicari langsung
      Bukan untuk mengecilkan pentingnya pekerjaan di sini, hanya maksudnya mungkin ini bisa dibuat lebih mudah diakses pembaca yang lebih luas. Meski begitu, saya juga tidak sedang mengusulkan hal absurd yang akan dianggap konyol semua orang di sini, seperti menulis ulang backend inferensi ke C#
    • adapted from arm limited optimized routine, jadi pada akhirnya ini memang berdiri di atas bahu para raksasa
    • Rasanya ini bukan sesuatu yang diajarkan di kuliah analisis asimtotik
      Saya jadi teringat profesor yang terkenal berkata, “konstanta yang diabaikan semua orang itu, secara rekayasa, bisa memakan seluruh kepalamu”
  • Katanya ini menggantikan lookup table short[65536], dan saya jadi bertanya apakah itu pilihan yang agak tumpul sejak awal
    Itu pada dasarnya menaruh lookup table sebesar seluruh cache L1; apakah ini bekerja cukup baik hanya karena secara probabilistik kebetulan pas?

    • Alasan lookup table bekerja lumayan baik justru karena beban kerjanya sendiri sangat tidak ramah cache
      Mengosongkan cache L1 tidak terlalu masalah, dan data yang tersingkir untuk memberi ruang pada LUT hampir tidak akan dipakai lagi
      Beban machine learning umumnya adalah streaming load yang membaca seluruh dataset secara linear pada setiap iterasi
    • Tulisan tentang kenapa sebaiknya mungkin tidak memakai lookup table https://specbranch.com/posts/lookup-tables/ membahas kapan hal itu secara umum tepat digunakan
      Dari pengalaman saya yang terbatas, Anda bisa melakukan cukup banyak komputasi real-time sebelum akhirnya lebih cepat daripada lookup
  • Di llama.cpp, ini pembahasan untuk CPU

    • Awalnya dikembangkan untuk llamafile, dan sudah masuk dalam dua rilis terbaru: https://github.com/Mozilla-Ocho/llamafile/releases/tag/0.8.2
      Sekarang ini sedang di-upstream ke proyek llama.cpp
      Ada juga peningkatan performa lain yang saat ini hanya tersedia di llamafile, misalnya pekerjaan Kawrakow yang membuat K quants jauh lebih cepat
  • Mungkin agak di luar topik, tetapi adakah yang tahu bagaimana ggml dibandingkan dengan runtime seperti tensorflow lite atau onnxruntime?

    • Saya cukup paham karena mengelola pustaka ONNX dan Flutter llama.cpp di keenam True Platforms
      Singkatnya, untuk LLM pakailah llama.cpp, dan dengan GGML sebagai dependensi intinya Anda juga bisa menjalankan whisper
      Selain itu, pakai ONNX saja
      TF itu seperti Apple di dunia machine learning: luar biasa jika Anda sepenuhnya terikat pada ekosistem Google ML, tetapi di luar itu pada dasarnya sudah mati. Proporsi model HF yang konyol, kira-kira 94%, adalah PyTorch
      Perbandingan performa inferensi langsung yang cukup layak mungkin hanya Whisper di ONNX versus GGML, dan seseorang pernah menjalankan pustaka llama.cpp saya bersama Whisper tanpa melaporkan perbedaan performa yang berarti
    • Penting untuk tahu ini merujuk ke hardware yang mana
  • Untuk inferensi non-batched pada perangkat CUDA saat ini, apakah gguf/llama.cpp adalah solusi dengan performa lebih baik, atau exllamav2+flashattention masih unggul?

    • Di 2x 4090, perbedaannya bisa diabaikan
      Ada perbedaan lain yang lebih penting, seperti KV cache 4-bit
  • LUT juga bisa divektorkan
    https://www.intel.com/content/www/us/en/docs/intrinsics-guid...
    Saya juga pernah menulis tentang hal-hal yang bisa dilakukan dengan LUT https://darkcephas.blogspot.com/2018/10/validating-utf8-stri...

    • Betul, tetapi bahkan jika Anda mengimplementasikan exp secara langsung, tergantung akurasi yang diinginkan, biasanya cukup 10~20 FMA
      Sulit bagi gather atau permutation untuk bersaing dengan komputasi murni
  • Dalam konteks yang mirip, ada juga tanh yang lebih cepat https://github.com/microsoft/onnxruntime/pull/20612

    • Pekerjaan yang luar biasa
      Tetapi apa targetnya? Apakah ini untuk mempercepat aproksimasi GeLU itu?
      Jika kembali ke erff(), sepertinya itu mungkin akan jauh lebih cepat
  • Apakah ini juga membantu untuk kasus penggunaan partial offloading GPU di gguf?
    Apakah sisi CPU juga jadi lebih cepat?