2024년 3월 16일 토요일

GPU 없는 로컬에서 서비스 가능한 경량 소형 LLM, LLAMA2.c, llama.cpp 빌드, 실행, 학습 및 코드 분석하기

이 글은 GPU 없는 컴퓨터에서 경량 LLAMA2.c (라마씨), llama.cpp 를 빌드하고, 로컬에서 실행, 학습하는 방법을 간략히 따라해 본다.
개요
앞서 설명한 코드 라마2는 최소 28GB이상의 GPU RAM이 필요하다. 많은 개발자의 경우, 현재 시점에서 이 정도 수준의 GPU 스펙 장비를 가진 경우는 매우 드물다. CoLab의 경우 T4 GPU는 16GB 크기를 가진다. 그러므로, 이를 해결할 수 있는 솔류션이 개발되었다. 

LLAMA2.c는 OpenAI 엔지니어인 Andrej Karpathy가 개발한 라마2를 이용한 소형 LLM 오픈소스 프로젝트이다. 이 라마 코드는 순수 C로 개발되었으며, 모델은 겨우 100MB로 동작되어, GPU가 없는 로컬 PC에서도 가볍게 실행된다.

llama.cpp는 불가리아의 개발자 게오르기 게르가노프(Georgi Gerganov)가 2023년 3월, 고가의 GPU 없이도 일반 사용자 환경에서 거대 언어 모델(LLM)을 효율적으로 실행하려는 목적으로 시작한 오픈소스 프로젝트이다. 

파이썬 의존성을 완전히 제거하고 순수 C/C++로 구현하여 이식성을 극대화했으며, 자체 개발한 GGML 라이브러리와 GGUF 파일 포맷을 통해 메모리 사용량과 연산 효율을 획기적으로 최적화했다. 특히 모델의 가중치를 압축하는 양자화 기술을 도입하여, 수십 GB에 달하는 거대 모델을 가정용 PC의 제한된 RAM에서도 원활히 구동할 수 있도록 구현함으로써 로컬 AI의 대중화를 이끌었다. 

LLAMA.c 설치, 사용법 및 분석
LLAMA.c 설치
설치, 실행 및 학습 방법은 다음과 같다. 터미널에서 다음 명령을 실행한다.
cd llama2.c
wget https://huggingface.co/karpathy/tinyllamas/resolve/main/stories15M.bin

이 결과, https://huggingface.co/datasets/roneneldan/TinyStories 으로 학습된 모델이 다운로드된다.

실행하기
코드를 컴파일하고, 다운로드받은 스토리 학습 모델을 실행한다. 
make run
./run stories15M.bin

실행 결과는 다음과 같다. 텍스트가 잘 생성되는 것을 확인할 수 있다.

좀 더 큰 모델을 다운받아 실행해 본다.
wget https://huggingface.co/karpathy/tinyllamas/resolve/main/stories42M.bin
./run stories42M.bin
./run stories42M.bin -t 0.8 -n 256 -i "One day, Lily met a Shoggoth"


다음과 같이, 접두어, 추가 명령줄 인수를 지정할 수도 있다.
-t: 생성 온도. 0~1.0
 
데이터 학습 방법
새로운 데이터 훈련을 위해서는 앞에서 사용한 스토리 모델이 어떻게 학습되었는지 확인해 볼 필요가 있다. 그 방법은 다음과 같다.
python tinystories.py download
python tinystories.py pretokenize
python train.py

학습 데이터 다운로드 화면 일부

이제 컴파일을 한다. 
make run

다음과 같이 실행하면 된다. 
./run stories15M.bin

코드 및 학습 데이터 분석하기
LLAMA2.c의 구조는 매우 간단한데, make파일을 분석해 보면 단 하나의 run.c를 컴파일해 실행파일을 빌드하는 것을 확인할 수 있다.
gcc -O3 -o run run.c -lm

run.c의 주요 부분을 살펴보면 다음과 같다. 
typedef struct {
    // token embedding table
    float* token_embedding_table;    // (vocab_size, dim)
    // weights for rmsnorms
    float* rms_att_weight; // (layer, dim) rmsnorm weights
    float* rms_ffn_weight; // (layer, dim)
    // weights for matmuls. note dim == n_heads * head_size
    float* wq; // (layer, dim, n_heads * head_size)
    float* wk; // (layer, dim, n_kv_heads * head_size)
    float* wv; // (layer, dim, n_kv_heads * head_size)
    float* wo; // (layer, n_heads * head_size, dim)
    // weights for ffn
    float* w1; // (layer, hidden_dim, dim)
    float* w2; // (layer, dim, hidden_dim)
    float* w3; // (layer, hidden_dim, dim)
    // final rmsnorm
    float* rms_final_weight; // (dim,)
    // (optional) classifier weights for the logits, on the last layer
    float* wcls;
} TransformerWeights;

이 구조체의 이름은 TransformerWeights이며, 이는 Transformer 모델의 가중치를 저장하는 데 사용된다. 트랜스포머의 어텐션 스코어를 계산하는 데 핵심적인 자료구조이다. 각 필드는 다음과 같은 의미를 가진다:
  • token_embedding_table: 토큰 임베딩 테이블을 나타낸다. 이는 어휘 크기(vocab_size)와 차원(dim)의 2차원 배열이다.
  • rms_att_weight, rms_ffn_weight: RMSNorm의 가중치를 나타낸다. 각각 attention과 feed-forward network에 대한 것이다.
  • wq, wk, wv, wo: 각각 query, key, value, output에 대한 가중치를 나타낸다. 이들은 multi-head attention에서 사용된다.
  • w1, w2, w3: feed-forward network에서 사용되는 가중치이다.
  • rms_final_weight: 최종 RMSNorm의 가중치를 나타낸다.
  • wcls: (선택적으로) 마지막 레이어에서 로짓에 대한 분류기 가중치를 나타낸다.
  • 이 구조체는 Transformer 모델의 가중치를 저장하고 관리하는 데 사용된다.
전체적인 프로그램 실행 구조는 다음과 같다. 

1. 이 코드는 트랜스포머 모델의 구성, 가중치 및 상태에 대한 구조와 메모리 할당, 메모리 해제 및 체크포인트 읽기 기능을 정의한다.
2. 'rmsnorm', 'softmax', 'matmul'과 같은 신경망 블록을 위한 함수도 포함되어 있다.
3. 코드는 Transformer 모델을 통해 입력 토큰을 처리하여 출력 로짓을 생성하는 'forward' 함수를 구현한다.
4. 또한 BPE(Byte Pair Encoding) 토크나이저가 문자열을 토큰으로 또는 그 반대로 변환하는 구조와 함수가 있다.
5. 코드에는 argmax, 샘플링 및 top-p 샘플링과 같은 다양한 전략을 사용하여 확률을 기반으로 토큰을 샘플링하는 '샘플러' 구조와 함수가 포함된다.
6. 시간 측정을 위한 유틸리티 기능과 Transformer 모델을 기반으로 텍스트를 생성하는 '생성' 기능을 정의한다.
7. 마지막으로 트랜스포머 모델을 사용하여 사용자와 어시스턴트 간의 대화를 시뮬레이션하는 '채팅' 기능을 정의한다.  
전체 프로그램 구조도

앞에서 사용된 1.5GB가 넘는 스토리 학습 데이터의 구조는 다음과 같이 되어 있다.
학습 데이터 구조 일부

이를 본인의 목적에 맞게 적절히 데이터를 준비한 후, 다음과 같이 동일한 방식으로 학습하면 가중치 벡터가 저장된 bin파일을 얻을 수 있다. 이를 이용해, 앞서 언급된 방식과 동일하게 사용하면 된다.

llama.cpp 윈도우 버전 설치 및 사용법
빌드 및 설치
NVIDIA 드라이버, 컴파일러 등 설치되었다고 가정하고 진행한다. 윈도우 버전 llama.cpp 빌드를 위해서는 visual studio 2022 버전을 설치해야 한다. 

그리고, 터미널에서 다음 순서대로 진행한다.

1. "Developer Command Prompt for VS 2022" 실행
2. git clone https://github.com/ggml-org/llama.cpp.git
3. Navigate to your llama.cpp folder (cd F:\projects\tools\llama.cpp).
4. cmake -B build -DGGML_CUDA=ON

CMake를 사용하여 `build` 폴더를 성공적으로 구성(Configure)하셨다면, 이제 실제로 코드를 컴파일하고 실행 파일(.exe)을 생성하는 빌드(Build) 단계를 진행해야 한다. `cmake -B build` 명령어는 "설정(Configure)"만 한 것이고, 실제 빌드는 다음 명령어로 수행한다. 

5. cmake --build build --config Release

`--build build`: 아까 생성한 `build` 디렉토리를 타겟으로 빌드를 수행한다.
`--config Release`: 성능 최적화를 위해 반드시 Release 모드로 빌드해야 한다. (이 옵션을 안 주면 기본값인 Debug 모드로 빌드되어 실행 속도가 매우 느리다.) 


빌드가 완료되면 실행 파일들은 `build` 폴더 안의 특정 경로에 생성된다.

예를 들면, `F:\projects\tools\llama.cpp\build\bin\Release` 일 것이고, 이곳에 `llama-cli.exe`, `llama-quantize.exe`, `llama-server.exe` 등이 생성되었는지 확인한다.

CPU 코어 수가 많다면 병렬 빌드를 사용하여 속도를 높일 수 있다. 싱글모드에서는 빌드에 최소 한두시간 걸릴 수 있다.
cmake --build build --config Release --parallel 4

실행 및 사용하기
생성된 실행 파일이 GPU(CUDA)를 제대로 인식하는지 확인하려면 `llama-cli.exe`를 실행해 본다.
build\bin\Release\llama-cli.exe -h

경로가 위와 다를 경우, `dir /s /b *.exe` 명령어로 파일 위치를 찾는다.
실행 시 로그 출력 중 "CUDA" 또는 "GGML_CUDA=1"과 같은 문구가 포함되어 있는지 확인하면 된다.

이제 빌드된 `llama-cli.exe`를 사용하여 모델 파일을 로드하고 실행한다.

이제 'qwen2.5:7b-instruct' 모델을 사용하여 테스트할 차례이다. 'llama.cpp'로 Qwen 모델을 실행하려면 모델 파일이 '.gguf' 형식이어야 한다.

1. 모델 파일 준비 (GGUF 형식)
'llama.cpp'는 GGUF 형식의 모델 파일만 지원한다. HuggingFace에서 'Qwen2.5-7B-Instruct-GGUF' 모델을 다운로드해야 한다.

Qwen/Qwen2.5-7B-Instruct-GGUF(https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF) 접속
'q4_k_m.gguf' 같은 파일을 다운로드하여 'F:\projects\tools\llama.cpp\models' 폴더(없으면 생성)에 저장

2. GPU 가속을 활용한 모델 실행
GPU를 사용하여 모델을 로드하려면 '-ngl' (number of gpu layers) 옵션을 사용해야 한다. 7B 모델은 대부분 GPU 메모리에 들어간다.

.\build\bin\Release\llama-cli.exe -m F:\projects\tools\llama.cpp\models\qwen2.5-7b-instruct-q3_k_m.gguf -ngl 99 -c 2048 -p "안녕? 자기소개 해줘." 
'-m': 모델 파일 경로
'-ngl 99': 모든 레이어를 GPU(CUDA)에 할당합니다. (GPU 메모리가 부족하다면 숫자를 30, 50 등으로 낮추세요.)
'-c 2048': 컨텍스트 윈도우 크기입니다.
'-p': 프롬프트 내용입니다.
정상 실행 결과
GPU 사용 체크

3. 테스트 체크리스트

1. CUDA 로그 확인: 실행 직후 로그에 'ggml_cuda_init: GGML_CUDA_DLL_PATH = ...', 'GGML_CUDA: Found 1 CUDA devices'와 같은 메시지가 뜨는지 확인. 이게 떠야 GPU를 쓰고 있는 것.
2. 성능 확인: 'eval time' 및 'sample time' 항목에서 'tokens per second' 수치를 확인. CPU만 사용할 때보다 훨씬 빠른 속도가 나와야 정상.
3. 메모리 확인: 모델이 로딩될 때 작업 관리자의 GPU 메모리(VRAM) 점유율이 올라가는지 확인.

만약 실행 시 "CUDA error"가 발생하거나 "Out of memory"가 뜬다면, GPU 메모리가 부족한 것이므로 '-ngl' 값을 줄여서(예: '-ngl 20') 다시 시도해 보시기 바란다.

서버 실행 방법
'llama-server'는 모델을 로드하여 로컬 API 서버(OpenAI API 호환)로 만들어준다. 이렇게 하면 웹 UI나 다른 프로그램에서 API 호출을 통해 모델을 사용할 수 있다.

1단계: 서버 실행 명령어 구성
'llama-cli'와 마찬가지로 '-ngl' 옵션을 통해 GPU 가속을 반드시 포함해야 한다.
터미널(빌드된 경로 'build\bin\Release' 또는 'llama.cpp' 루트)에서 아래 명령어를 실행한다.

.\build\bin\Release\llama-server.exe -m F:\projects\tools\llama.cpp\models\qwen2.5-7b-instruct-q3_k_m.gguf -ngl 99 --port 8080 --host 0.0.0.0

'-ngl 99': 모델을 최대한 GPU VRAM에 로딩.
'--port 8080': 서버가 실행될 포트.
'--host 0.0.0.0': 외부 접속을 허용 (로컬에서만 쓸 거면 생략 가능). 

2단계: 서버 상태 확인
명령어를 실행하면 로그가 쭉 올라갑니다. 마지막 부분에 다음과 비슷한 내용이 나오면 서버가 정상적으로 뜬 것이다.

llama_model_load: ...
...
HTTP server listening at 0.0.0.0:8080

이제 브라우저를 열고 'http://localhost:8080'에 접속해 본다. 'llama.cpp' 자체적으로 제공하는 내장 웹 UI가 나타나며, 여기서 바로 채팅 테스트가 가능하다.

3단계: API 테스트 (Python 예시)
'llama-server'는 OpenAI API 규격을 따르기 때문에, Python에서 'openai' 라이브러리로 쉽게 연동할 수 있다.

from openai import OpenAI

# 서버가 8080 포트에서 실행 중일 때
client = OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="not-needed" # 로컬이므로 아무 값이나 입력
)

response = client.chat.completions.create(
    model="Qwen2.5",
    messages=[
        {"role": "user", "content": "Qwen 2.5 모델에 대해 설명해줘."}
    ]
)

print(response.choices[0].message.content)

4단계: 트러블슈팅 및 팁
  • GPU 사용 확인: 서버가 실행되는 동안 터미널 로그를 보면 '[llama.cpp] ... CUDA ...' 같은 문구가 보일 것이다. 만약 속도가 매우 느리다면 모델 전체가 VRAM에 로드되지 않은 것이니, '--gpu-layers' (또는 '-ngl') 설정을 다시 확인한다.
  • 메모리 관리: 서버는 모델을 메모리에 계속 올려두고 대기한다. 모델 사용이 끝나면 터미널에서 'Ctrl + C'를 눌러 서버를 확실히 종료하여 GPU VRAM을 회수하라.
  • 컨텍스트 최적화: 서버 구동 시 '-c 4096' 또는 '-c 8192' 옵션을 추가하여 컨텍스트 윈도우를 조정할 수 있다 (모델의 최대 지원 길이에 맞춰 설정).

레퍼런스

댓글 없음:

댓글 쓰기