제9회 EVO-X2 (Ryzen AI Max+ 395 / 128GB) + Qwen3.8-Flash-Next용 llama.cpp 개조 재개 및
요약
본 글은 llama.cpp를 기반으로 한 LLM 추론 엔진의 성능 최적화 및 개조 과정을 다룹니다. 특히 ROCm과 Vulkan 백엔드를 사용하여 PLE 테이블 분할 기법을 적용한 결과를 비교 분석했습니다. 그 결과, 컨텍스트 크기(Context)에 따라 PP와 TG 속도가 더 빠른 백엔드가 달라지는 것을 확인했습니다.
핵심 포인트
- upstream llama.cpp 기반으로 패치 추가 방식을 변경하여 유지보수성을 높임.
- ROCm과 Vulkan을 비교한 결과, Context 크기에 따라 최적의 백엔드가 다름.
- PLE 테이블 16분할 기법 적용 후, ROCm이 PP에서 우위, Vulkan이 TG에서 우위를 보임.
지난번에는 휴가 며칠 전에 바쁘게 llama.cpp 개조를 시작하게 되었고, 그대로 약 2주간의 휴가를 보내면서 완전히 방치했습니다.
돌아와 보니 본래의 llama.cpp도 Halogen도 여러 가지가 새롭게 바뀌어 있었고, Strata라는 굉장히 좋아 보이는 추론 엔진도 나와 있었습니다. Qwen3.8-Flash-Next를 빠르게 구동하고 싶다면, 직접 무언가를 하려 하기보다는 llama.cpp나 다른 추론 엔진의 업데이트를 기다리는 것이 좋을 것 같은 느낌이 들었지만, llama.cpp 개조 자체가 재미있어지고 있는 부분이라 조금 더 가지고 놀아보기로 했습니다.
휴가 전의 개조는 LaurentZuijdwijk의 Strix Halo 전용 fork를 기반으로 했었는데, 본래(upstream)의 llama.cpp가 계속 새롭게 업데이트되다 보니, 기반이 오래되면 최신 업데이트를 지속적으로 통합하기 어려울 것 같아서 전체 구성을 재검토하기로 했습니다.
이후로는 Prompt Processing (입력을 읽어들이는 처리)를 PP, Token Generation (출력 생성)을 TG로 표기하겠습니다. 둘 다 단위는 tok/s이며, 높을수록 빠릅니다.
본래(upstream)의 llama.cpp를 기반으로 하고, 개조 항목별로 작은 패치를 만들어서 추가해 나가는 방식으로 변경했습니다. upstream에 큰 업데이트가 들어올 때는, 기반만 교체하고 필요한 패치만 다시 적용할 수 있도록 할 계획입니다.
이번에 사용한 기반은 다음과 같습니다.
-
upstream:
ggml-org/llama.cpp -
build:
b11247 -
commit:
0bc845d356f437d5ce4fe975c36428f7522829cb
지금까지는 Vulkan 버전의 빌드만 해본 적이 있었지만, 비교를 위해 ROCm 버전도 구동하고 싶어서 Windows 환경에서 ROCm 10.0 / TheRock을 사용한 자체 빌드를 진행했습니다.
그리고 기반 upstream llama.cpp에 제8회 때 가장 처음 했던 'PLE 테이블을 16분할하여 GPU에 올리는' 부분까지 이식한 상태로, Vulkan / ROCm을 64k, 128k, 256k에서 비교해 보니 다음과 같았습니다.
| Backend | Context | PP | TG |
|---|---|---|---|
| Vulkan | 64k | 267.90 | 17.20 |
| ROCm | 64k | 357.93 | 14.86 |
| Vulkan | 128k | 159.56 | 12.00 |
| ROCm | 128k | 259.41 | 9.91 |
| Vulkan | 256k | 103.75 | 7.30 |
| ROCm | 256k | 167.35 | 5.74 |
제6회 unsloth 버전 llama.cpp (b10798-mix-659e406) 시점에서는, 제 환경에서 PP와 TG 모두 Vulkan 버전이 더 빨랐습니다.
이번에는 반대로, PP는 ROCm이, TG는 Vulkan이 빠릅니다.
64k의 경우 ROCm의 PP가 Vulkan보다 약 33.6% 높고, TG는 Vulkan이 약 15.7% 높습니다. 128k와 256k에서는 차이가 더욱 커집니다.
ROCm 빌드에 막히거나 측정용 스크립트를 정비하느라 예상보다 시간이 걸려서, 'PLE 테이블을 16분할하여 GPU에 올리는' 부분의 이식까지 마쳤습니다.
속도 최적화에는 아직 착수하지 못했습니다.
소스 코드, Windows 빌드, 검증 스크립트 등은 여기서 공개하고 있습니다.
main 브랜치는 Strix Halo 전용 fork 기반의 r2 안정 버전으로 일단 동결하고, 이번부터의 작업은 r3/upstream-first 브랜치에서 진행하고 있습니다.
속도와 관련된 부분만 먼저 말씀드립니다.
| 항목 | 내용 |
|---|---|
| PC | GMKtec NucBox EVO-X2 |
| ... | |
| 상세 내용은 기사 말미에 정리하겠습니다. |
당초 계획으로는, 휴가 후에 휴가 전 제8회 기사 게시물 이후 시도했던 MTP나 다른 고속화 항목들 중에서 효과가 있었던 것만 골라 통합하는 것부터 시작할 예정이었습니다.
하지만 Chappy님께 upstream의 llama.cpp, Halogen 및 기타 관련 정보를 조사해 달라고 부탁드렸더니, 휴가 기간 동안 상황이 여러모로 바뀌어 있었습니다.
MTP나 원래 개조 후보였던 항목들이 이미 upstream에 통합되어 있거나, Halogen이 더욱 빨라졌거나, Strata라는 새로운 엔진이 나와 있는 등 말입니다.
Halogen이나 Strata가 저희 EVO-X2 Windows에서 그대로 구동될 수 있는 상황이었다면, llama.cpp 개조는 미루고 그쪽을 먼저 시도해 봤을 것 같지만, 그렇지 않았기 때문에 당분간은 llama.cpp의 개조를 계속하기로 했습니다.
지난번 LaurentZuijdwijk의 Strix Halo 전용 fork 기반 개조판에 추가적인 수정을 가하게 되면, upstream llama.cpp에 좋은 수정 사항이 들어올 때마다 개별적으로 통합해야 하는 번거로움이 생깁니다.
게다가, upstream 측에서 비슷한 처리가 구현되었을 경우, 구형 fork에 적용했던 자체 구현과 이중으로 관리될 가능성도 있습니다.
나중에 Qwen4 등 다른 모델 세대로 넘어갈 때 처음부터 다시 해야 하는 것도 싫기 때문에, 초기 수고가 들더라도
upstream llama.cpp 그 자체를 '아무것도 추가하지 않은 기준'으로 삼고, 필요한 차이점(diff)만 작은 패치 형태로 쌓는 방식으로 변경했습니다.
이번의 베이스는 다음과 같습니다.
upstream: ggml-org/llama.cpp
build: b11247
commit: 0bc845d356f437d5ce4fe975c36428f7522829cb
...
이 시점에서는,
- r2의 QSA union 없음
- PLE16 loader 없음
- Unsloth MTP 호환성 없음
- MTP-QSA prototype 없음
처럼, 의도적으로 아무것도 추가하지 않은 상태입니다.
여기서 필요한 변경 사항만 COMMON-xxx / VULKAN-xxx / ROCM-xxx와 같은 관리 ID를 붙여가며 추가하기로 했습니다.
지금까지는 속도 측정용, VRAM 사용량 등 리소스 측정을 위한 PowerShell 스크립트를 사용해 왔는데, 측정할 때마다 리소스 측정과 속도 측정을 별도의 PowerShell 창에서 수동으로 실행하고, 출력 로그 파일 이름도 일부를 수동으로 입력해야 하는 상황이었습니다.
그래서,
파일 이름에는 64k라고 적혀 있는데 실제 구동 조건은 128k 같은 실수가 발생하기도 했습니다.
이에 r3에서는 측정 시스템을 먼저 정리했습니다.
Current 스크립트는 아래에 있습니다.
대략적인 구성은,
tools/evox2/
├─ build/
│ ├─ Build-Vulkan.ps1
...
Measure-LlamaCli.ps1에서는 실제로 사용하는 llama-cli.exe로부터 다음 항목들을 자동으로 기록하도록 했습니다.
- 백엔드(backend) / 장치(device)
- llama.cpp 빌드 번호, 커밋, 컴파일러
- 실행 파일 해시(executable hash)
- 컨텍스트 등 실행 조건
- PP / TG / 토큰 수
- MTP 승인 여부(MTP acceptance)
- 리소스 로그
1회 측정마다,
conditions.json
result.json
summary.csv
...
같은 파일들을 모아서 저장합니다.
더 나아가 Invoke-BenchmarkMatrix.ps1을 통해 잠자고 있거나 자리를 비운 동안에도 여러 컨텍스트 길이 및 수정 패치 ON/OFF 패턴을 한 번에 돌릴 수 있게 했습니다.
덤으로 빌드 과정까지 PowerShell 스크립트화하여 사용된 소스 커밋, 컴파일러, 백엔드 등의 정보를 매니페스트(manifest)로 남기도록 했습니다.
이렇게 해두면 나중에 AI 에이전트로 완전히 자동화하고 싶을 때도 수월할 것 같습니다.
우선은 베이스 upstream llama.cpp에 개조를 가하지 않은 상태로 빌드하여 기준 속도를 측정하기로 했습니다. 속도 측정은 평소의 한국어 장문 요약 작업을 진행했습니다.
Vulkan 버전은 제7회에서 처음 빌드했고, 제8회의 개조판도 문제없이 빌드할 수 있었기 때문에 그대로 PowerShell 스크립트화해서 간단히 빌드할 수 있었습니다.
소스는 베이스 llama.cpp를 그대로 사용하고, PLE16 변환 전의 오리지널 Unsloth UD-IQ3_XXS 모델, 컨텍스트 길이 64k / MTP OFF 조건에서는,
Vulkan
PP 265.10 tok/s
TG 16.75 tok/s
이었습니다.
비교를 위해 ROCm 버전도 빌드해 보고 싶어졌습니다.
제6회 unsloth 버전 llama.cpp (b10798-mix-659e406) 기준으로, PP와 TG 모두 Vulkan 버전이 더 빨랐고, 이전 기반으로 삼았던 Strix Halo 전용 fork가 Vulkan 전용이었기 때문에 직접 ROCm 버전을 빌드해 본 적은 없었습니다.
여러 가지 어려움을 겪었던 이야기는 나중에 설명하겠지만, 최종적으로 Windows 환경에서 다음 구성으로 작동했습니다:
ROCm 10.0.0 / TheRock
AMD Clang 23.0.0
GPU target: gfx1151
Vulkan 버전과 동일한 소스, 모델, 컨텍스트 길이로 테스트했을 때,
ROCm
PP 359.84 tok/s
TG 14.50 tok/s
이었습니다.
| Backend | PP | TG |
|---|---|---|
| Vulkan | 265.10 | 16.75 |
| ROCm | 359.84 | 14.50 |
같은 Windows, 같은 EVO-X2, 같은 llama.cpp 소스임에도 불구하고, PP는 ROCm이 상당히 빠릅니다. 반면, TG는 Vulkan 쪽이 더 빠릅니다.
사전 빌드 버전인 b11243 ROCm 바이너리로 측정했을 때는,
PP 358.42 tok/s
TG 14.42 tok/s
이었기 때문에, 자체 빌드한 b11247 ROCm의 359.84 / 14.50은 상당히 근접합니다.
빌드 번호가 다르므로 엄밀한 성능 비교는 아니지만, 적어도 '직접 빌드했더니 뭔가 이상한 구성이 되어 극단적인 수치가 나왔다'는 느낌은 아닌 것 같습니다.
우선적으로 제8회에서 가장 먼저 진행했던 'PLE 테이블을 16개로 분할하여 GPU에 올리는' 부분을 이식했습니다.
이 부분은 지난번에도 속도 향상 자체와는 큰 관련이 없었지만, 지난번 개조 후 측정은 기본적으로 Unsloth UD-IQ3_XXS 모델의 PLE16 변환 버전과 AgentionAI 버전을 사용하고 있습니다.
앞으로 개조할 것(GitHub의 r3 브랜치)을 지난번(GitHub의 r2 브랜치)과 비교하기 쉽도록 하기 위해서도, 처음에 필요했던 작업이었습니다.
참고로 PLE16은 16비트화가 아닙니다.
원래 크기가 큰 PLE n-gram 텐서를 16개의 헤드 단위로 분할한 형태로, 모델 변환에는 지난번과 마찬가지로 Laurent Zuijdwijk의 Strix Halo 전용 fork에 포함되어 있던
ggu-py
ggu
scripts
ggu_split_ple_heads.py
을 사용했습니다.
이번에는 이 분할된 텐서를 읽어올 로더를 이식했습니다.
이 변경 사항에는 GitHub 문서 상에 COMMON-001이라는 관리 ID를 붙였습니다.
커밋은 다음과 같습니다:
902e9f4a762cc0fe89d613868c35e4861610081e
qwen4exp: support split PLE n-gram tensors
구현 방식으로는,
ple_ngram_embd.N.weight
이라는 분할 텐서를 감지하고, 16개의 헤드를 읽어와 각 헤드의 로컬 행 인덱스로 변환한 후 gather하여 qwen4exp 쪽 그래프로 되돌립니다.
※ AgentionAI 버전을 불러오기 위해서는 ROCmFP4 대응이라는 별도의 개조가 필요하기 때문에, 이번 측정은 Unsloth UD-IQ3_XXS 모델의 PLE16 변환 버전만으로 진행했습니다.
| Context | Backend | PLE16 구현 전 PP | PLE16 구현 후 PP | PP 차이 | PLE16 구현 전 TG | PLE16 구현 후 TG | TG 차이 |
|---|---|---|---|---|---|---|---|
| 64k | Vulkan | 265.10 | 267.90 | +1.1% | 16.75 | 17.20 | +2.7% |
| ... |
PLE16을 넣어도 기본적인 경향은 바뀌지 않았습니다. TG의 차이는 오차 범위인지 애매한 부분이지만, 지금은 깊이 파고들지 않기로 했습니다.
- PP는 ROCm이 빠름
- TG는 Vulkan이 빠름
- TG는 컨텍스트가 길어질수록 상당히 떨어짐
- 특히 ROCm의 TG 하락 폭이 큼
여기서 지난번 측정값과 비교해 보겠습니다.
지난번(r2)은 Strix Halo 전용 fork + upstream 선택 통합 + QSA union까지 들어간 상태였습니다. 반면, 지금(r3)은 아직 그 속도 향상을 이식하지 않았고, COMMON-001 (Ple16 테이블 대응)만 이식한 상태입니다.
r3 Vulkan은 QSA union을 아직 포팅하지 않았기 때문에, 긴 글(long context)일수록 r2와 차이가 크게 난다.
ROCm은 아무런 가속화도 추가하지 않았는데도 64k, 128k에서는 r2 Vulkan을 능가했다. 256k에서도 r2의 177.01 tok/s에 비해 167.35 tok/s까지 나와 있다.
PP만 놓고 보면,
혹시 직접 개조하지 않아도 ROCm으로 상당히 빠른 것이 아닌가?
라는 생각이 들기도 하지만, 개조를 추가하면 더 빨라질 수도 있을 것 같다.
TG는 지난번(r2)과 이번(r3)을 비교해 보면 다음과 같다.
| Context | r2 Vulkan TG | r3 Vulkan TG | r3 ROCm TG |
|---|---|---|---|
| 64k | 23.55 | 17.20 | 14.86 |
| ... | |||
| ※128k TG는 제8회 기사 본문에는 게재하지 않았다. 가지고 있던 측정값 2회분(21.72 / 21.57 tok/s)을 평균한 값. |
TG가 이상하게 느려졌다.
지난번에 진행했던 QSA union은 주로 PP를 위한 것이었고, TG는 거의 변하지 않았기 때문에, QSA union 포팅 전이라서 그런 것은 아닌 것 같다.
원래의 전용 fork 쪽에 TG를 위한 개선이 있는 건지, 아니면 upstream 쪽 구현 변화로 긴 글 디코드 시 불필요한 처리가 늘어난 건지.
이는 다음번에 자세히 조사하기로 했다.
이번에 예상외로 시간이 오래 걸린 것이 Windows 버전 ROCm의 직접 빌드였다.
최종적으로 작동한 구성은 다음과 같았다.
Windows 11
ROCm 10.0.0 / TheRock
AMD Clang 23.0.0
...
처음에 원래 환경을 전혀 바꾸지 않고 빌드를 시도했을 때, configure 단계에서
clang++: error: cannot find ROCm device library
으로 멈췄다. 이는 HIP SDK 7.2를 사용하고 있었기 때문이었다.
프리빌드 버전은 ROCm 10.0이라고 되어 있고, 7.2를 수정해서 애쓰는 것도 싫었기에, ROCm 10.0 / TheRock으로 마이그레이션했다.
ROCm 10.0은 설치 파일을 다운로드해서 클릭만 할 수 있는 방식이 아니라, 이 절차를 따라 python의 venv로 가상 환경을 만들고 pip install해야 했다. (자세한 내용은 'TheRock 환경' 항목에서 설명한다.)
ROCm 10.0으로 바꾸면 바로 될 줄 알았는데, 이번에는 Visual Studio 2026 / MSVC 14.51 환경에서 첫 HIP 소스 acc.cu를 컴파일했을 때,
isgreater
isless
isunordered
처럼 HIP 측의 수학 함수와 MSVC의 <cmath>가 충돌하며 멈췄다.
대책은 Visual Studio 2026 Build Tool과는 별도로, 새로 Visual Studio 2022 Build Tools를 추가하여 MSVC 14.44를 사용하는 것이었다.
Visual Studio 2022는 구 버전이라 다운로드하려면 여기에서 마이크로소프트 계정으로 로그인해야 했다.
ROCm 10.0 / TheRock의 설치 명령어는 최종적으로
python -m venv C: herockuild\.venv
& C: herockuild\.venv
Scripts
Activate.ps1
python -m pip install `
...
device-gfx1151가 중요했다.
처음에는
rocm[libraries,devel]
만 넣고 llama.cpp 자체의 빌드는 성공했다.
그런데 llama-cli로 추론하려고 하니,
rocBLAS error: Cannot read .../rocblas/library/TensileLibrary.dat
No such file or directory for GPU arch : gfx1151
으로 멈췄다.
즉, 빌드에 필요한 ROCm 일체와, gfx1151에서 실제로 rocBLAS를 구동하기 위한 디바이스 패키지는 별개였다.
device-gfx1151
을 추가하자 정상적으로 작동했다.
빌드 시에는 Visual Studio 2022의 'x64 Native Tools Command Prompt'에서
powershell -ep bypass
로 powershell을 실행한 후, ROCm 10.0 / TheRock을 설치한 가상 환경을 활성화하기 위해 매번
C:\TheRock\build\.venv\Scripts\Activate.ps1
를 실행해야 했다. 그 후 congfigure
cmake -S . -B .\build-rocm-b11247 -G "Ninja Multi-Config" \
"-DCMAKE_PREFIX_PATH=$RocmPath" \
-DGGML_BACKEND_DL=ON \
...
이후 ggml-hip만 빌드하고,
cmake --build .\build-rocm-b11247 \
--config Release \
--parallel 4 \
...
이 잘 되면, llama-cli 등의 나머지 툴도 빌드한다.
cmake --build .\build-rocm-b11247 \
--config Release \
--parallel 4 \
...
빌드가 통과되어 끝이라고 생각했지만,
ROCm0: AMD Radeon(TM) 8060S Graphics (0 MiB, 0 MiB free)
이 나왔다. GPU 이름은 보이지만 VRAM이 0 MiB인 것이다.
원인은 PATH에 TheRock 측을 넣었더라도 System32 측의 AMD HIP runtime이 먼저 감지되는 경우였기 때문이다.
최종적으로 ROCm 10.0 측의,
amdhip64_7.dll
rocm_kpack.dll
amd_comgr.dll
을 llama-cli.exe와 같은 디렉터리로 복사하자
Available devices:
ROCm0: AMD Radeon(TM) 8060S Graphics (110456 MiB, 110301 MiB free)
로 정상적으로 인식했다.
마지막으로,
& "$Bin\test-backend-ops.exe" test -b ROCm0 -o FLASH_ATTN_EXT
을 실행했다.
결과는
3982/3982 tests passed
Backend ROCm0: OK
이었다.
빌드 시에 경고(warning)가 많이 나오지만, 이번 목적은 '업스트림 b11247을 최대한 그대로의 상태로 베이스라인으로 만드는 것'이므로, 경고를 없애기 위한 소스 수정이나 컴파일러 플래그 추가는 하지 않았다.
결과적으로 실 모델인 llama-cli도 정상 작동하여 64k 길이의 긴 입력까지 완주했다.
이를 통해 Vulkan과 ROCm을 같은 소스에서 직접 빌드하여 비교할 수 있는 기반이 마련되었다.
이번에는 환경만 갖추었을 뿐, 고속화 개조는 아무것도 진행되지 않았다.
오히려 지난번 효과가 있었던 QSA union도 아직 이식하지 못했고, TG(Tensor Group) 관련해서는 상당히 늦어지고 있다.
뭔가 후퇴하는 것처럼 보일 수도 있지만, 어느 정도 오래 지속하려면 이런 정리가 필요하다고 생각한다.
만약 휴가가 없고 지난번의 기세 그대로 밀고 나갔다면, 지금쯤 진흙탕에 빠져 있었을지도 모른다.
다음 번에는 TG가 늦어진 원인을 조사하고 수정하는 것부터 시작할 예정이다.
휴가 전, 제9회 글 게시 후에도 MTP를 넣어보거나 다른 고속화를 시도하다 거절한 등 여러 가지가 있었지만, 그것은 나중에 별도의 글로 정리하기로 했다.
.\llama-cli.exe \
-m "(모델의 경로)" \
-c (컨텍스트 길이) \
...
이번에도 지난번까지와 같은 긴 입력 데이터를 사용했다.
실제 입력 토큰 수는,
| context | 실입력 |
|---|---|
| 64k | 61,789 tokens |
| ... | |
| 256k에서는 요구 컨텍스트 262,144의 약 97.3%까지 입력했다. |
지난번 글과 내용이 같으므로 상세 내용은 생략한다.
-
장치(Device): GMKtec NucBox EVO-X2
-
CPU / APU: AMD Ryzen AI MAX+ 395 with Radeon 8060S
-
Windows가 표시하는 CPU 클럭: 3.00 GHz
-
설치된 메모리: 128 GB
-
UMA 프레임 버퍼: 96 GB
-
Windows CPU 측에서 보이는 RAM: 약 32 GB
-
GPU: AMD Radeon 8060S Graphics
-
GPU 아키텍처(arch):
gfx1151 -
OS: Windows 11 Pro
-
버전: 25H2
-
OS 빌드: 26200.9168
-
설치일: 2025-10-24
-
Windows 기능 경험 팩(Feature Experience Pack): 1000.26100.344.0
-
상위 저장소(Upstream repository):
ggml-org/llama.cpp -
빌드(Build):
b11247 -
커밋(Commit):
0bc845d356f437d5ce4fe975c36428f7522829cb -
커밋 제목(Commit title):
vulkan : reuse descriptor sets when bindings are constant (#29280) -
r3 브랜치:
r3/upstream-first
Vulkan:
- 컴파일러(Compiler): LLVM / Clang 20.1.8
- Vulkan SDK: 1.4.357.0
ROCm:
- ROCm: 10.0.0 / TheRock
- 컴파일러(Compiler): AMD Clang 23.0.0
- 호스트 툴체인(Host toolchain): Visual Studio 2022 Build Tools / MSVC 14.44
- GPU 타겟(GPU target):
gfx1151
902e9f4a762cc0fe89d613868c35e4861610081e
qwen4exp: support split PLE n-gram tensors
-
저장소(Repository):
unsloth/Qwen3.8-Flash-Next-GGUF -
양자화(Quantization):
UD-IQ3_XXS -
llama.cpp가 보고하는 파일 크기: 76.32 GiB
-
BPW: 3.71
-
모델 파라미터(Model params): 176.94B
상기 모델을 LaurentZuijdwijk의 Strix Halo 전용 fork 내에 있는
gguf-py exttt{gguf} exttt{scripts} exttt{gguf_split_ple_heads.py}
으로 PLE16 형식으로 변환한 모델을 사용했다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기