1 điểm bởi GN⁺ 2024-05-16 | 1 bình luận | Chia sẻ qua WhatsApp
  • PR #7154 của llama.cpp đã viết lại phần tính toán SiLU và SoftMax cho CPU của GGML bằng triển khai dựa trên expf() đã được vector hóa của llamafile, và được merge vào master ngày 17/05/2024
  • GGML trước đây dùng bảng tra cứu short[65536] để tăng tốc, nhưng triển khai mới hướng tới phép tính chính xác hơn, trong khi vẫn giữ sai số làm tròn tệ nhất ở mức 2 ULP trên aarch64 và SSE2+
  • Trong bài kiểm thử hiệu năng CPU SOFT_MAX, SSE2+FMA nhanh hơn 1,5 lần, AVX2+FMA nhanh hơn 1,9 lần, AVX512 nhanh hơn 2,1 lần; kết quả trên AMD Ryzen 9 5950X và M2 Ultra cũng xác nhận nhanh hơn khoảng 1,5 lần so với master
  • Thay đổi bao gồm bổ sung ggml_v_expf(), ggml_v_silu(), tách mã trùng lặp thành ggml_vec_soft_max_f32(), loại bỏ các hàm liên quan đến GGML_SILU_FP16, và điều chỉnh nhánh SiLU có điều kiện cho SSE2 hoặc ARM NEON
  • Sau khi merge, kết quả không xác định đã được tái hiện khi chạy server với >1 slots; về sau nguyên nhân được thu hẹp là do -ffinite-math-only, dẫn tới ràng buộc ở mức build rằng cần dùng -fno-finite-math-only

Mục tiêu thay đổi của PR và trạng thái merge

  • PR #7154, với tiêu đề ggml : rewrite silu and softmax for cpu, viết lại phần tính toán SiLU và SoftMax trong đường xử lý CPU của GGML thuộc llama.cpp
  • Thay đổi bắt đầu dưới dạng upstream hàm expf() đã được vector hóa của llamafile
  • PR được merge vào ggml-org:master ngày 17/05/2024, và commit merge được ghi là 934266c
  • Tác giả cho biết cách mới có thể tính SoftMax và SiLU chính xác hơn so với bảng tra cứu short[65536] mà GGML trước đây dùng để tăng tốc

Độ chính xác và phạm vi hỗ trợ

  • Đường xử lý mới dựa trên expf() hỗ trợ aarch64 và SSE2+, với sai số làm tròn tệ nhất được nêu là 2 ULP
  • Trong mô tả ban đầu, các triển khai AVX2 và AVX512 cũng đã được viết, nhưng không được đưa vào vì lợi ích không đủ lớn để đánh đổi độ phức tạp mã so với SSE2+FMA
  • Sau đó, dựa trên kết quả benchmark, mã AVX2 và AVX512 cũng được đưa vào
  • Một đầu ra kiểm thử riêng nêu 4294967296 numbers tested successfully, kèm so sánh kết quả giữa exp và triển khai của llamafile trên nhiều giá trị đầu vào

Phạm vi thay đổi mã

  • Các thay đổi chính được reviewer tóm tắt như sau
    • Xóa #define đã bị comment
    • Tách 5 dòng trùng lặp thành ggml_vec_soft_max_f32()
    • Xóa nhiều hàm liên quan đến GGML_SILU_FP16
    • Thêm ggml_v_expf()
    • Thêm ggml_v_silu()
    • Điều chỉnh tiền xử lý để ggml_vec_silu_f32() dùng hàm khác nhau tùy theo cờ SSE2 hoặc __ARM_NEON
  • Số lượng file thay đổi được hiển thị trong metadata của GitHub là 1 file
  • PR được gắn nhãn refactoringReview Complexity : High; nhãn sau đi kèm mô tả rằng có thể cần kiến thức sâu về LLM hoặc GPU

Benchmark và kết quả hiệu năng

  • ggerganov xác nhận SOFT_MAX trên AMD Ryzen 9 5950X và M2 Ultra nhanh hơn khoảng 1,5 lần so với master
  • Lệnh kiểm thử được sử dụng như sau
make -j tests && ./tests/test-backend-ops -o SOFT_MAX -b CPU perf
  • Sau đó, tác giả cho biết với cùng lệnh, lợi thế hiệu năng tăng lên như sau
    • SSE2+FMA: 1,5 lần
    • AVX2+FMA: 1,9 lần
    • AVX512: 2,1 lần
  • Một script phát triển riêng đưa ra các số liệu sau
    • 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 llama.cpp server trên GitHub Actions ghi nhận 543 iterations với cấu hình phi-2 q4_0 trên Standard_NC4as_T4_v3
    • Người dùng đồng thời: 8
    • duration: 10 phút
    • Trung bình request HTTP: 8626.19ms
    • p95: 21696.44ms
    • Trung bình xử lý prompt: 94.59 tk/s
    • Trung bình sinh token: 33.43 tk/s

Thảo luận tối ưu hóa AVX512

  • chriselrod đề xuất dùng vscalefps trên AVX512
    • vscalefps tính zmm0 = zmm1 * 2^{zmm2}
    • Được cho là có thể xử lý overflow và underflow phù hợp, nhờ đó loại bỏ checks và blends
  • Ví dụ triển khai bằng Julia và vòng lặp assembly đã được chia sẻ; nếu kiểm thử đúng, sai số tối đa tại x=47.483456fdưới 1 ULP
  • Cách tiếp cận vscalefps không dùng lookup table; với triển khai Float64/double, nó dùng lookup table 16 phần tử thông qua vpermi2pd
  • Sau đó, liên kết triển khai C++ cũng được chia sẻ
    • ExpAVX512
    • Mã nguồn nằm trong include/ExpAVX512.hpp
    • README có benchmark, nhưng cho biết không benchmark so sánh với các triển khai khác

Vấn đề không xác định sau khi merge

  • Sau khi merge, một trường hợp tái hiện được báo cáo: khi dùng >1 slots trên server, kết quả trở nên không xác định
  • Quy trình tái hiện tối thiểu như sau
make clean && make server
./server -m models/opt/llama_2-7b-q4_0.gguf --parallel 2 --threads 1
  • Request chạy ở shell khác như sau
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
  • Token probabilities của token cuối cùng luân phiên giữa hai giá trị sau mỗi lần gọi curl; nếu dùng 4 slots thì luân phiên giữa bốn giá trị khả dĩ

-ffinite-math-only và ràng buộc build

  • Các commit liên quan sau đó tham chiếu việc thu hẹp nguyên nhân vấn đề về -ffinite-math-only
  • Vấn đề này được ghi nhận là có thể do SiLU trả về NaN hoặc giá trị rác khác thay vì flush các giá trị nhỏ về 0
  • Bản fix kiểm tra xem -fno-finite-math-only đã được đặt hay chưa, và ép buộc kiểm tra rằng chế độ biên dịch không được là finite math mode
  • Thông báo lỗi hướng dẫn rằng một số routine của GGML cần non-finite math arithmetic, và cần truyền -fno-finite-math-only cho compiler
  • Sau đó, người dùng chia sẻ kinh nghiệm rằng -Ofast hoặc -ffast-math có thể bao gồm -ffinite-math-only và làm hỏng build
    • Có báo cáo rằng tới GCC 13.2 vẫn có thể dùng -Ofast, nhưng từ GCC 14 thì kết quả trở thành rác
    • Trong một số kiểm thử, ngoài -fno-finite-math-only còn cần cả -fmath-errno
    • Nhiều commit tiếp theo ở các repository khác tham chiếu việc loại bỏ -ffast-math hoặc chỉ định rõ -fno-finite-math-only để giải quyết lỗi biên dịch ggml

1 bình luận

 
GN⁺ 2024-05-16
Ý kiến trên Hacker News
  • Khoảng 20 năm trước, khi lập trình cho bộ xử lý tín hiệu radar Hughes, tôi cần tính e^x trong phạm vi 0 < x < 1
    Bộ xử lý đó có phép nhân, nên tôi tạo 4 bảng e^x, mỗi bảng gồm 256 giá trị khả dĩ cho từng khối 8 bit trong 4 khối 8 bit của một word 32 bit, rồi nhân chúng lại để ra giá trị cuối cùng
    Nó nhanh hơn routine e^x tốt nhất trước đó khoảng 5 lần, và dù giờ đã lỗi thời, đó là một cỗ máy thú vị từng xử lý tín hiệu radar nhanh hơn nhiều bộ xử lý trên danh nghĩa là nhanh hơn trong một thời gian

    • Nếu khó theo kịp thì ý tưởng đại khái là e^x = e^(a+b+c+d), trong đó a/b/c/d là từng byte của x; chuyển thành e^a * e^b * e^c * e^d rồi tạo từng bảng tra cứu e^a, e^b
      Nói nghiêm ngặt thì a có dạng như high byte << 24, nên bảng e^a sẽ là ánh xạ a => e^(a<<24), và các byte khác cũng được xử lý tương tự
  • Tôi tò mò những cải tiến silu và softmax như thế này ảnh hưởng đến tốc độ suy luận LLM tổng thể đến mức nào
    Nếu tôi sai thì mong được sửa, nhưng vì phần lớn thời gian dùng cho nhân ma trận nên có lẽ tác động của thay đổi này sẽ nhỏ

    • Đúng là phần lớn phép toán dấu phẩy động dùng cho nhân ma trận, nhưng softmax dùng băng thông bộ nhớ nhiều một cách mất cân đối, nên thường mất thời gian lâu hơn nhiều so với dự đoán nếu chỉ nhìn vào số phép toán
  • Hơi lạc đề một chút, nhưng khi lướt qua tôi đã nghĩ “cái này trông như một tối ưu hóa khá điên rồ. Mã phức tạp và đã được nhiều người xem rồi”, rồi nhìn người đóng góp thì thấy “đúng là jart. Những lời giải điên rồ mà hay ho lúc nào cũng là jart”

    • Lý do nó trông đáng sợ chủ yếu là vì cú pháp intrinsics của C/C++ vốn như vậy
      Cũng như nhiều thứ trong mảng đó, nỗi đau này phần nào là tự mình gây ra
      Tôi biết có các thư viện C++ cho phép cú pháp SIMD kiểu C# và hardware intrinsic, nhưng nhược điểm là khó tra cứu trực tiếp mnemonic trong tài liệu tập lệnh
      Tôi không có ý hạ thấp tầm quan trọng của việc làm ở đây, chỉ muốn nói rằng có lẽ nó đã có thể dễ tiếp cận hơn với nhóm độc giả rộng hơn. Tuy vậy tôi chưa định đưa ra đề xuất mà hẳn mọi người ở đây sẽ thấy kỳ quặc là viết lại backend suy luận bằng C#
    • adapted from arm limited optimized routine cơ à, cuối cùng cũng là đứng trên vai người khổng lồ
    • Tôi nghĩ mấy thứ này không phải nội dung được dạy trong lớp phân tích tiệm cận
      Nhớ đến câu nói nổi tiếng của một giáo sư: “cái hằng số mà mọi người đều phớt lờ ấy, trong kỹ thuật nó có thể ăn sạch cả cái đầu của bạn”
  • Nói là thay thế bảng tra cứu short[65536], nhưng ngay từ đầu đó chẳng phải là một lựa chọn hơi chậm chạp sao?
    Tức là đặt một bảng tra cứu bằng cả kích thước L1 cache; có phải do xác suất tình cờ khá khớp nên nó lại hoạt động tốt bất ngờ không?

    • Lý do bảng tra cứu hoạt động tốt ngoài dự đoán là vì bản thân workload cực kỳ không thân thiện với cache
      Thổi bay L1 cache cũng không quan trọng lắm, và dữ liệu bị đẩy ra để nhường chỗ cho LUT đằng nào cũng gần như không được tái sử dụng
      Tải máy học nhìn chung là streaming load, đọc tuyến tính toàn bộ dataset ở mỗi vòng lặp
    • Bài viết về lý do vì sao có lẽ không nên dùng bảng tra cứu https://specbranch.com/posts/lookup-tables/ bàn về khi nào nó phù hợp nói chung
      Theo kinh nghiệm hạn hẹp của tôi, bạn có thể tính toán realtime khá nhiều trước khi nó chậm hơn lookup
  • Trong llama.cpp, đây là chuyện dành cho CPU

    • Ban đầu nó được phát triển cho llamafile, và đã có trong hai bản phát hành gần đây: https://github.com/Mozilla-Ocho/llamafile/releases/tag/0.8.2
      Giờ họ đang upstream nó vào dự án llama.cpp
      Hiện cũng có những cải tiến hiệu năng khác chỉ có trong llamafile, ví dụ công việc của Kawrakow giúp K quants nhanh hơn nhiều
  • Có thể hơi lạc đề, nhưng có ai biết những thứ như ggml so với các runtime như tensorflow lite, onnxruntime thì thế nào không?

    • Tôi duy trì ONNX và thư viện Flutter llama.cpp trên cả 6 True Platforms, nên khá rành
      Nói ngắn gọn: với LLM thì dùng llama.cpp là đúng, và với GGML là phụ thuộc cốt lõi thì cũng chạy được whisper
      Ngoài ra thì dùng ONNX
      TF giống như Apple của giới máy học: nếu bạn hoàn toàn bị khóa trong hệ sinh thái Google ML thì rất tuyệt, nhưng bên ngoài đó thì thực tế gần như đã chết. Một tỷ lệ vô lý các mô hình HF, khoảng 94%, là PyTorch
      So sánh hiệu năng suy luận trực tiếp đáng làm có lẽ là Whisper của ONNX với GGML, nhưng từng có người chạy thư viện llama.cpp của tôi cùng Whisper và không báo cáo khác biệt hiệu năng đáng kể nào
    • Quan trọng là đang nói chính xác đến phần cứng nào
  • Ở thời điểm hiện tại, với suy luận không batching trên thiết bị CUDA, gguf/llama.cpp có phải là giải pháp hiệu năng tốt hơn không, hay exllamav2+flashattention vẫn chiếm ưu thế?

    • Trên 2x 4090 thì khác biệt không đáng kể
      Có những khác biệt quan trọng hơn, như KV cache 4 bit
  • LUT cũng có thể vector hóa
    https://www.intel.com/content/www/us/en/docs/intrinsics-guid...
    Trước đây tôi cũng từng viết về những thứ có thể làm với LUT https://darkcephas.blogspot.com/2018/10/validating-utf8-stri...

    • Đúng, nhưng nếu tự triển khai exp thì cũng chỉ cần khoảng 10–20 FMA tùy độ chính xác mong muốn
      gather hay permutation khó cạnh tranh với tính toán thuần túy
  • Cùng mạch đó, cũng có tanh nhanh hơn https://github.com/microsoft/onnxruntime/pull/20612

    • Công việc rất tốt
      Nhưng mục tiêu là gì nhỉ? Là làm cho phép xấp xỉ GeLU đó nhanh hơn sao?
      Nếu quay lại erff() thì có lẽ sẽ nhanh hơn nhiều
  • Cái này có giúp cho trường hợp dùng offload một phần GPU của gguf không?
    Phía CPU cũng nhanh hơn à?