- llama.cpp PR #7154는 GGML의 CPU용 SiLU와 SoftMax 계산을 llamafile의 벡터화된
expf() 기반 구현으로 다시 작성해 2024년 5월 17일 master에 병합됨
- 기존 GGML은 속도를 위해
short[65536] 조회 테이블을 사용했지만, 새 구현은 aarch64와 SSE2+에서 최악의 반올림 오차를 2 ULP로 유지하며 더 정확한 계산을 목표로 함
SOFT_MAX CPU 성능 테스트에서 SSE2+FMA는 1.5배, AVX2+FMA는 1.9배, AVX512는 2.1배 빨라졌고, AMD Ryzen 9 5950X와 M2 Ultra에서도 master 대비 약 1.5배 빠른 결과가 확인됨
- 변경에는
ggml_v_expf(), ggml_v_silu() 추가, ggml_vec_soft_max_f32()로 중복 코드 추출, GGML_SILU_FP16 관련 함수 제거, SSE2 또는 ARM NEON 조건부 SiLU 경로 조정이 포함됨
- 병합 후
>1 slots 서버 실행에서 비결정적 결과가 재현됐고, 이후 -ffinite-math-only가 원인으로 좁혀져 -fno-finite-math-only가 필요하다는 빌드 차원의 제약으로 이어짐
PR의 변경 목표와 병합 상태
- PR #7154는
ggml : rewrite silu and softmax for cpu라는 제목으로, llama.cpp의 GGML CPU 경로에서 SiLU와 SoftMax 계산을 다시 작성함
- 변경은 llamafile의 벡터화된
expf() 함수를 upstream하는 형태로 시작됨
- PR은 2024년 5월 17일
ggml-org:master에 병합됐고, 병합 커밋은 934266c로 표시됨
- 작성자는 기존 GGML이 속도를 위해 사용하던
short[65536] 조회 테이블보다 새 방식이 SoftMax와 SiLU를 더 정확하게 계산할 수 있다고 밝힘
정확도와 지원 범위
- 새
expf() 기반 경로는 aarch64와 SSE2+ 를 지원하며, 최악의 반올림 오차는 2 ULP로 제시됨
- 초기 설명에서는 AVX2와 AVX512 구현도 작성됐지만, SSE2+FMA 대비 코드 복잡도를 감수할 만큼의 이점이 크지 않아 포함하지 않았다고 함
- 이후 벤치마크 결과를 바탕으로 AVX2와 AVX512 코드도 포함됨
- 별도 테스트 출력에서는
4294967296 numbers tested successfully가 제시됐고, 여러 입력값에 대해 exp와 llamafile 구현의 결과 비교가 포함됨
코드 변경 범위
- 리뷰어가 정리한 주요 변경 사항은 다음과 같음
- 주석 처리된
#define 제거
- 중복된 5줄을
ggml_vec_soft_max_f32()로 추출
GGML_SILU_FP16 관련 여러 함수 제거
ggml_v_expf() 추가
ggml_v_silu() 추가
ggml_vec_silu_f32()가 SSE2 또는 __ARM_NEON 플래그에 따라 다른 함수를 쓰도록 전처리문 조정
- 변경 파일 수는 GitHub 메타데이터상 1개로 표시됨
- PR에는
refactoring과 Review Complexity : High 라벨이 붙었고, 후자는 LLM 또는 GPU에 대한 깊은 지식이 필요할 수 있다는 설명을 포함함
벤치마크와 성능 결과
ggerganov는 AMD Ryzen 9 5950X와 M2 Ultra에서 SOFT_MAX가 master보다 약 1.5배 빠르다고 확인함
- 사용한 테스트 명령은 다음과 같음
make -j tests && ./tests/test-backend-ops -o SOFT_MAX -b CPU perf
- 이후 작성자는 같은 명령에서 성능 이점이 다음과 같이 증가한다고 밝힘
- SSE2+FMA: 1.5배
- AVX2+FMA: 1.9배
- AVX512: 2.1배
- 별도 개발용 스크립트에서는 다음 수치가 제시됨
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의
llama.cpp server 벤치마크는 Standard_NC4as_T4_v3에서 phi-2 q4_0 구성으로 543 iterations를 기록함
- 동시 사용자: 8
- duration: 10분
- HTTP 요청 평균: 8626.19ms
- p95: 21696.44ms
- Prompt processing 평균: 94.59 tk/s
- Token generation 평균: 33.43 tk/s
AVX512 최적화 논의
chriselrod는 AVX512에서 vscalefps 사용을 제안함
vscalefps는 zmm0 = zmm1 * 2^{zmm2}를 계산함
- overflow와 underflow를 적절히 처리해 checks와 blends를 제거할 수 있다고 함
- Julia 구현 예시와 어셈블리 루프가 공유됐고, 테스트가 맞다면
x=47.483456f에서 최대 오차가 1 ULP 미만이었다고 함
vscalefps 접근은 lookup table을 쓰지 않으며, Float64/double 구현에서는 vpermi2pd를 통한 16원소 lookup table을 사용한다고 설명됨
- 이후 C++ 구현 링크도 공유됨
- ExpAVX512
- 소스는
include/ExpAVX512.hpp에 있음
- README에는 벤치마크가 포함됐지만, 다른 구현과 비교 벤치마크는 하지 않았다고 함
병합 후 비결정성 문제
- 병합 후 서버에서
>1 slots 사용 시 비결정적 결과가 나온다는 재현 사례가 보고됨
- 최소 재현 절차는 다음과 같음
make clean && make server
./server -m models/opt/llama_2-7b-q4_0.gguf --parallel 2 --threads 1
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가
curl 호출마다 두 값 사이를 순환했고, 4 slots를 쓰면 네 가능한 값 사이를 순환한다고 함
-ffinite-math-only와 빌드 제약
- 이후 관련 커밋들은
-ffinite-math-only를 문제 원인으로 좁힌 내용을 참조함
- 해당 문제는 SiLU가 작은 값을 0으로 flush하는 대신 NaN 또는 다른 garbage 값을 반환하는 것으로 추정됐다고 기록됨
- fix는
-fno-finite-math-only가 설정됐는지 확인하고, 컴파일 모드가 finite math 모드가 아니어야 한다는 검사를 강제함
- 에러 메시지는 GGML 일부 루틴이 non-finite math arithmetic을 필요로 하며, 컴파일러에
-fno-finite-math-only를 전달하라고 안내함
- 이후 사용자들은
-Ofast 또는 -ffast-math가 -ffinite-math-only를 포함해 빌드를 깨뜨릴 수 있다는 경험을 공유함
- GCC 13.2까지는
-Ofast를 사용할 수 있었지만 GCC 14부터 결과가 garbage가 됐다는 보고가 있음
- 일부 테스트에서는
-fno-finite-math-only 외에 -fmath-errno도 필요했다고 함
-ffast-math를 제거하거나 -fno-finite-math-only를 명시해 ggml 컴파일 오류를 해결한 후속 커밋들이 여러 저장소에서 참조됨