📌 전체 구조 요약
이 코드는 ggml로 간단한 행렬 곱셈(matmul)을 수행하는 예제입니다:
- CPU 또는 GPU(Metal / CUDA) backend에서 연산 수행
- ggml 계산 그래프 구성 → 실행 → 결과 확인
- ggml의 “그래프 기반 연산 모델” 구조를 보여주는 좋은 예제
🗺️ 구조 단계별 분석
1️⃣ 준비
#include "ggml.h"
#include "ggml-alloc.h"
#include "ggml-backend.h"
- ggml core, 메모리 할당기, backend API 사용
2️⃣ 모델 정의
struct simple_model {
struct ggml_tensor* a{};
struct ggml_tensor* b{};
ggml_backend_t backend = nullptr;
ggml_backend_buffer_t buffer{};
struct ggml_context* ctx{};
};
- simple_model 구조체에 2개의 텐서 a, b 선언
- backend 설정 (CPU / CUDA / Metal)
- backend buffer 사용 (a, b 데이터를 저장)
- context는 ggml에서 텐서 메타 정보, 메모리 관리를 담당
3️⃣ 데이터 준비
float matrix_A[4 * 2] = { ... };
float matrix_B[3 * 2] = { ... };
- matrix_A: (4×2) matrix
- matrix_B: (3×2) matrix → 내부에서 transpose되어 곱해짐
4️⃣ Backend 초기화
#ifdef GGML_USE_CUDA
model.backend = ggml_backend_cuda_init(0);
#endif
#ifdef GGML_USE_METAL
model.backend = ggml_backend_metal_init();
#endif
if (!model.backend) {
model.backend = ggml_backend_cpu_init();
}
- CUDA → Metal → CPU 순으로 backend 초기화
- llama.cpp도 비슷한 구조 (다양한 backend 지원)
5️⃣ ggml context 생성 + 텐서 생성
ggml_init_params params{...};
model.ctx = ggml_init(params);
model.a = ggml_new_tensor_2d(model.ctx, GGML_TYPE_F32, 2, 4);
model.b = ggml_new_tensor_2d(model.ctx, GGML_TYPE_F32, 2, 3);
- ggml context 생성 → 텐서 생성
ggml_new_tensor_2d
로 2D tensor 생성 (shape = cols, rows 주의)
6️⃣ Backend buffer 사용
model.buffer = ggml_backend_alloc_ctx_tensors(model.ctx, model.backend);
ggml_backend_tensor_set(model.a, a, ...);
ggml_backend_tensor_set(model.b, b, ...);
- backend buffer에 텐서 데이터를 직접 업로드
- CPU → GPU 메모리 복사 직접 컨트롤 → llama.cpp와 동일한 설계 철학
7️⃣ 계산 그래프 구성
ggml_context* ctx0 = ggml_init(params0);
ggml_cgraph* gf = ggml_new_graph(ctx0);
ggml_tensor* result = ggml_mul_mat(ctx0, model.a, model.b);
ggml_build_forward_expand(gf, result);
- 별도의 temporay context 생성 → 계산 그래프 구성
ggml_mul_mat
: 행렬 곱 연산 노드 생성- 그래프 빌드 단계 → llama.cpp에서도 필수
8️⃣ 메모리 예약 + 그래프 실행
ggml_gallocr_alloc_graph(allocr, gf);
ggml_backend_graph_compute(model.backend, gf);
- 메모리 할당기(allocator)로 그래프 최적화 실행
- backend에서 그래프 실행 → CPU든 GPU든 동일한 API 사용 (llama.cpp에서도 동일)
9️⃣ 결과 가져오기 + 출력
ggml_backend_tensor_get(result, out_data.data(), ...);
- backend 메모리 → CPU 메모리로 결과 복사
- 결과 행렬 출력
10️⃣ 마무리 정리
ggml_gallocr_free(allocr);
ggml_free(model.ctx);
ggml_backend_buffer_free(model.buffer);
ggml_backend_free(model.backend);
- 메모리 및 backend clean-up
🚩 핵심 포인트
✅ ggml는 그래프 기반 연산을 사용
✅ 메모리 컨트롤과 backend 제어를 직접적으로 노출
✅ llama.cpp와 구조적으로 동일:
- llama.cpp도
ggml_graph_compute
사용 - backend abstraction 사용
- memory 관리 explicit
✅ on-device 최적화에 매우 유리한 구조
🔍 총평
- 매우 좋은 ggml 샘플
- llama.cpp의 내부 구조 이해에 딱 적합
- “C/C++ 기반 + 그래프 기반 + backend abstraction” → llama.cpp가 임베디드에 강한 이유 그대로 보여줌
🚀 추가로 볼만한 확장 방향
1️⃣ ggml_compute_forward vs backend_graph_compute 차이 → CPU 전용 vs multi-backend 지원 차이
2️⃣ ggml-alloc의 역할 → runtime 메모리 최적화
3️⃣ gguf 파일 로딩 → llama.cpp에서는 gguf → ggml_tensor 구성 → ggml_graph 실행 흐름
🗺️ ggml-simple.cpp vs llama.cpp 구조 비교
🏗️ 1️⃣ 공통 기본 구조
단계 | ggml-simple.cpp | llama.cpp |
---|---|---|
Backend 초기화 | CUDA/Metal/CPU backend 선택 | 동일 (llama.cpp도 backend layer 존재) |
ggml_context 생성 | ggml_init(params) | ggml_init(params) (llama.cpp 내부 llama_init_from_file ) |
Tensor 구성 | ggml_new_tensor_2d 직접 호출 | 모델 로딩시 gguf → tensor 자동 구성 |
데이터 업로드 | ggml_backend_tensor_set 수동 업로드 | gguf 로딩시 자동 tensor 초기화 + backend buffer 업로드 |
계산 그래프 구성 | ggml_mul_mat → ggml_build_forward_expand | llama_decode_internal 내에서 여러 ggml_xxx 연산 호출 후 build |
메모리 할당 최적화 | ggml_gallocr_alloc_graph | llama_batch_decode 시 내부 alloc 사용 |
그래프 실행 | ggml_backend_graph_compute | ggml_backend_graph_compute (llama.cpp도 동일 호출) |
결과 추출 | ggml_backend_tensor_get | llama_decode_internal 에서 logits 추출시 사용 |
🖼️ 구조 흐름도 시각화
📌 ggml-simple.cpp
[main]
↓
[backend init] → [ggml_init]
↓
[create tensor A, B] → [upload tensor data]
↓
[build graph: ggml_mul_mat]
↓
[alloc graph memory]
↓ [compute graph] → [get result tensor]
📌 llama.cpp
(inference 경로)
[llama_init_from_file]
↓
[backend init] → [ggml_init]
↓
[load model.gguf → create all ggml_tensor]
↓
[upload tensor data to backend buffer]
↓
[user calls llama_decode or llama_batch_decode]
↓
[llama_decode_internal builds ggml_graph]
↓
[alloc graph memory (alloc + scratch)]
↓
[compute graph (ggml_backend_graph_compute)]
↓
[extract logits tensor → tokenizer / output]
🎁 핵심 차이점
✅ ggml-simple.cpp
는 수동으로 텐서 만들고 그래프 구성
✅ llama.cpp는 gguf 기반으로 텐서 자동 구성 + LLM 아키텍처(Attention/MLP/Norm 등) 구성
- llama.cpp 내부의 핵심 그래프는:
- ggml_mul_mat
- ggml_norm
- ggml_silu
- ggml_repeat
- ggml_add / ggml_mul / ggml_reshape 등
→ 이 모든 노드들이ggml_cgraph
로 빌드되고 실행됨 → 지금 올려주신 코드 흐름과 거의 동일한 구조
🚀 정리
- 지금 올려주신
ggml-simple.cpp
는 llama.cpp 구조 이해에 매우 좋은 샘플임 → llama.cpp 내부도 결국 ggml_graph를 구성해 실행하는 구조 - llama.cpp는:
- model.gguf 로드 시 → tensor 구성
- llama_decode 호출 시 → ggml_graph 구성
- ggml_backend_graph_compute로 실행
- 이식성 + 최적화 포인트도 동일하게 ggml 구조에 기반
🎨 추천 학습 순서
1️⃣ ggml-simple.cpp
에서 ggml_mul_mat → → MLP Layer 구성도 연습
2️⃣ llama.cpp의 llama_decode_internal
함수 분석
3️⃣ gguf
파일 구조 분석 (gguf = LLM weights + arch config + metadata)
4️⃣ llama.cpp에 커스텀 ggml 연산 추가 실험
🗺️ llama.cpp 최신(2025) llama_decode_internal
구조 흐름
1️⃣ llama_init_from_file
- gguf 모델 파일 로드
- 모든 ggml_tensor 생성 (weights, embeddings, norms 등)
- backend init + backend buffer alloc + tensor data 업로드
- → 기본적으로
ggml_new_tensor_xd
+ggml_backend_tensor_set
사용됨 →ggml-simple.cpp
와 동일
2️⃣ llama_decode_internal 호출
- 핵심 LLM “forward pass” 수행
[llama_decode_internal]
↓
[ggml_init graph context]
↓
[ggml_cgraph* gf = ggml_new_graph(ctx)]
↓
[build graph for N tokens → 1 layer씩 loop]
3️⃣ Per-layer graph 구성
For each Transformer layer:
[ggml_rms_norm] → Norm
↓
[ggml_mul_mat] Q_proj → Q
[ggml_mul_mat] K_proj → K
[ggml_mul_mat] V_proj → V
↓
[ggml_rope] apply rotary positional embedding to Q,K
↓
[ggml_mul_mat] Q x K^T → attention scores
[ggml_soft_max] attention scores → weights
[ggml_mul_mat] weights x V → attention output
↓
[ggml_add] residual connection
↓
[ggml_rms_norm] → Norm
↓
[ggml_mul_mat] FFN_1 (intermediate proj)
[ggml_silu] activation
[ggml_mul] gated activation
[ggml_mul_mat] FFN_2 (final proj)
↓
[ggml_add] residual connection
→ Transformer layer 1개 당 위와 같은 ggml node 그래프 구성
4️⃣ 전체 Graph 최종 구성
[embedding lookup]
↓
[layer loop: N layers → ggml nodes push]
↓
[ggml_norm + projection to logits]
↓
[ggml_cgraph build complete → ggml_build_forward_expand(gf, final_logits)]
5️⃣ 그래프 실행
ggml_backend_graph_compute(backend, gf);
- 바로 아까
ggml-simple.cpp
와 동일한 호출 - backend(CPU, CUDA, Metal 등)에 따라 최적화 실행
6️⃣ 결과 추출
ggml_backend_tensor_get(final_logits_tensor, ...)
- logits 결과 추출 후 softmax / sampling → output token 결정
🎁 정리
ggml-simple.cpp | llama.cpp 내부 구조 |
---|---|
ggml_mul_mat | ggml_mul_mat (Q_proj, K_proj, V_proj, FFN 등) |
ggml_build_forward_expand | ggml_build_forward_expand |
ggml_backend_graph_compute | ggml_backend_graph_compute |
tensor 수동 입력 | gguf로 자동 로드 후 tensor 구성 |
matmul 결과 출력 | logits 결과 출력 |
🚩 결론
✅ llama.cpp는 결국 ggml_graph 기반으로 모든 LLM forward pass를 구현
✅ 아까 올려주신 ggml-simple.cpp
가 구조적으로 거의 동일:
- Backend 초기화 → Tensor 구성 → Graph 구성 → Graph 실행 → 결과 출력
✅ llama.cpp에서 ggml_mul_mat / ggml_norm / ggml_silu / ggml_rope 등 기본 node들만 조합해서 LLM 완성
✅ 학습은 없고 inference-only라 그래프를 build → run → destroy cycle로 반복 (token마다 반복)
🎨 요약 그림
[Embedding] → [N layers (Attention + FFN)] → [Norm + Final projection] → [Logits] → [Sampling → Output token]