콘텐츠로 이동

1주차 — Homebrew & Ollama

Local LLM Study / 1주차

Ollama를 설치하고 로컬 GGUF 모델과 대화한 뒤, 서버 구조·LLM 추론 과정·Context별 메모리 할당을 순서대로 살펴봤다.

학습 목표

  • Local AI와 클라우드 기반 AI의 차이를 이해한다.
  • 터미널과 Homebrew를 이용하여 macOS 개발 환경을 구성한다.
  • Ollama 서버를 실행하고 로컬 HTTP API의 정상 동작을 검증한다.
  • 모델 파일을 로컬에 직접 준비한 뒤 Ollama에 등록하여 실제 추론을 수행한다.

실습 환경

항목 확인한 정보
운영체제 macOS
기기 MacBook Pro 14 · Apple M5 · 메모리 32GB
GPU 서버 로그에서 Apple M5 / Metal / iGPU 탐지
메모리 관련 탐지값 Ollama 로그에 total / available 25.0 GiB 표시
설치 방식 Homebrew Formula로 Ollama 설치
Ollama 버전 0.40.9
모델 Gemma 3 4B, GGUF Q4_0
Ollama 등록명 gemma3-local:latest

실습에 필요한 기본 개념

  • Local AI: 모델 추론을 사용자의 컴퓨터에서 수행하는 방식. 이미 준비한 로컬 모델은 원격 추론 서비스에 입력을 전달하지 않고 사용할 수 있다.
  • LLM: 입력 토큰을 바탕으로 다음 토큰을 반복적으로 예측하고 생성하는 언어 모델.
  • Homebrew: macOS에서 소프트웨어와 의존성을 설치·관리하는 패키지 관리자.
  • Ollama: 모델 파일을 등록·관리하고 로컬 추론 요청을 처리하는 실행 환경.

CLI와 서버의 관계

flowchart TD
    CLI["터미널 B: Ollama CLI<br/>pull / create / list / run"]
    SERVER["터미널 A: ollama serve<br/>계속 실행되는 서버 프로세스"]
    MODEL["로컬 모델 관리·추론 처리"]
    CLI -->|"HTTP 요청 · localhost:11434"| SERVER
    SERVER --> MODEL
    MODEL -->|"처리 결과"| SERVER
    SERVER -->|"HTTP 응답"| CLI

ollama serve는 계속 실행되는 서버 프로세스이고, ollama create, ollama list, ollama run 등은 별도 터미널에서 서버에 요청하는 CLI 명령이다. 서버 실행과 모델 등록 및 추론은 서로 다른 단계이다.

실습 — Ollama 설치부터 첫 대화까지

실습 1. Ollama 설치

brew update
brew install ollama
ollama --version

Homebrew 설치 과정에서 두 가지 서버 실행 방식을 확인했다.

같은 Ollama 서버를 실행하지만, 프로세스의 관리 주체와 생명주기가 다르다.

구분 brew services start ollama ollama serve
실행 방식 백그라운드 서비스 현재 터미널에서 직접 실행
프로세스 관리 macOS launchd 실행한 셸/터미널 세션
터미널 종료 계속 실행 일반적으로 함께 종료
사용자 로그인 시 자동 시작 O (일반 사용자 서비스로 등록한 경우) X
비정상 종료 시 재시작 서비스 등록·관리 설정에 따라 재시작 가능 자동 재시작하지 않음
로그 확인 서비스 로그 파일 등에서 확인 실행 중인 터미널에서 실시간 확인
종료 방법 brew services stop ollama Ctrl+C
적합한 상황 일상적인 상시 사용 학습, 디버깅, 설정 실험

brew services start ollama

Homebrew의 services 명령은 macOS의 launchd와 연동된다.

일반 사용자 권한으로 실행하면 서비스가 ~/Library/LaunchAgents에 등록되어 현재 실행되고, 다음 사용자 로그인 시에도 자동 시작되도록 설정된다.

서버를 상시 이용할 때 편리하다.

참고: Homebrew 공식 문서 — services

ollama serve

현재 터미널에서 Ollama 서버 프로세스를 직접 실행한다. 서버 로그를 실시간으로 관찰할 수 있으며 Ctrl+C로 종료할 수 있다.

이번 실습에서는 모델 등록 및 추론 시 서버 로그까지 분석하기 위해 이 방식을 선택했다.

주의: 한쪽 방식으로 이미 127.0.0.1:11434를 사용하고 있다면 다른 방식으로 Ollama 서버를 중복 실행하지 않는다. CLI의 ollama list, ollama create, ollama run은 서버가 실행 중인 상태에서 새 터미널에서 사용할 수 있다.

실습 2. 로컬 서버 실행

OLLAMA_NO_CLOUD=1 \
OLLAMA_FLASH_ATTENTION=1 \
OLLAMA_KV_CACHE_TYPE=q8_0 \
ollama serve

환경변수 의미:

  • OLLAMA_NO_CLOUD=1: Ollama의 클라우드 모델 기능을 비활성화하고 로컬 모델 중심으로 사용한다. 단, 이 설정 자체가 모든 네트워크 접속을 차단하는 방화벽은 아니다.
  • OLLAMA_FLASH_ATTENTION=1: 지원되는 추론 환경에서 Flash Attention 사용을 활성화한다.
  • OLLAMA_KV_CACHE_TYPE=q8_0: KV Cache에 q8_0 양자화 설정을 사용한다.

서버 시작 로그:

Ollama cloud disabled: true
Listening on 127.0.0.1:11434 (version 0.40.9)
inference compute ... library=Metal ... description="Apple M5" ... total="25.0 GiB"

이를 통해 클라우드 기능 비활성화 적용, 로컬 포트 바인딩, GPU 탐지를 확인했다.

GPU 탐지 로그만으로 실제 모델 추론의 GPU 사용률을 측정한 것은 아니다.

실습 3. 로컬 HTTP API 확인

서버는 실행 상태로 유지하고 다른 터미널에서 다음 명령을 실행했다.

curl http://127.0.0.1:11434/api/version
curl http://127.0.0.1:11434/api/tags

실제 출력:

{"version":"0.40.9"}

모델 등록 전 출력:

{"models":[]}

서버 로그에서는 두 요청 모두 HTTP 200으로 처리되었다.

models: []는 해당 시점에 로컬에 등록된 모델이 없다는 의미다.

실습 4. GGUF 모델 파일 직접 준비

ollama pull 방식뿐 아니라 모델 파일을 직접 다운로드해 Ollama에 등록하는 흐름도 실습하고자 했다.

다음 Hugging Face 저장소에서 GGUF 형식의 Gemma 3 4B 모델을 준비했다.

  • 모델 출처: ggml-org/gemma-3-4b-it-qat-GGUF
  • 사용 파일: gemma-3-4b-it-qat-Q4_0.gguf
  • mmproj 파일: 이미지 입력 처리에 관련된 파일. 이번에는 텍스트 대화만 수행하므로 사용하지 않았다.

모델 파일이 로컬에 준비되면 Modelfile의 FROM으로 해당 GGUF 파일을 참조할 수 있다.

FROM /path/to/gemma-3-4b-it-qat-Q4_0.gguf

위 FROM 경로와 아래 Modelfile 경로는 기존 문서에 남아 있는 예시 표기다.

실제 로컬 경로를 포함한 원본 명령은 이 글에 포함된 자료에서는 확보되지 않았다.

/path/to/Modelfile

실습 5. GGUF 모델 등록

서버를 계속 실행한 채 새 터미널에서 다음 명령을 사용했다.

ollama create gemma3-local -f /path/to/Modelfile

주요 출력:

gathering model components
copying file sha256:ee91c3e7a4ab95d8c95672f9fcb58bf236b257e9f217966bcf53a5a6df4ab49a 100%
parsing GGUF
using existing layer sha256:ee91c3e7a4ab95d8c95672f9fcb58bf236b257e9f217966bcf53a5a6df4ab49a
using autodetected template gemma3-instruct
creating new layer sha256:d3a76cb8c4a07d0a6c82ac6e839f98816b5077699d393b2cc77008c16d8078ac
writing manifest
success

등록 과정에서 Ollama가 GGUF 형식을 읽고 gemma3-instruct 템플릿을 자동 감지한 후 모델 Manifest를 기록했다.

서버의 HEAD /api/blobs/... 요청에 대해 중간에 404가 나타났으나, 이는 해당 Blob의 기존 존재 여부를 확인하는 과정에서 발생한 응답으로 볼 수 있으며 최종 ollama create는 success로 종료되었다.

실습 6. 등록 모델 확인 및 대화

ollama list

실제 출력:

NAME                   ID              SIZE      MODIFIED
gemma3-local:latest    048ab3b6a24f    2.5 GB    10 seconds ago

이어서 모델을 실행했다.

ollama run gemma3-local

실제 대화:

실제 대화

로컬 모델이 텍스트 입력에 응답하는 것을 확인했다.

실습 결과

검증 항목 결과
Ollama 실행 성공, 127.0.0.1:11434 리슨
버전 API HTTP 200, version 0.40.9
모델 목록 API (등록 전) HTTP 200, models: []
GGUF 등록 성공, gemma3-instruct 템플릿 감지
등록 후 모델 목록 gemma3-local:latest, 약 2.5 GB
실제 텍스트 추론 성공, 한국어 응답 생성

첫 단문 대화의 추론 로그

prompt eval time = 68.22 ms / 12 tokens (175.91 tokens per second)
eval time        = 261.87 ms / 11 tokens (38.19 tokens per second)
total time       = 330.09 ms / 23 tokens
  • Prompt Evaluation / Prefill: 입력 프롬프트 토큰을 처리하는 단계.
  • Token Evaluation / Decode: 뒤이어 응답 토큰을 생성하는 단계.

서버 로그 해석

  • POST /api/create: 모델 등록 요청.
  • GET /api/tags: 로컬에 등록된 모델 목록 조회.
  • POST /api/generate 및 POST /api/chat: 모델 추론을 요청하는 API 경로.
  • 로그에 표시된 slot, sampler chain, n_ctx_slot=32768, prompt cache 등은 모델 실행 내부를 더 깊게 분석할 때 살펴볼 단서.

심화 1단계 — Ollama 서버 구조와 요청 생명주기

CLI가 서버에 요청하고, 서버가 Runner를 실행하고 종료하는 흐름을 실제 프로세스와 단일 HTTP 요청으로 확인했다.

1-1. CLI → 서버 → Runner 프로세스 확인

이번 실습은 설치된 Ollama v0.40.9를 기준으로, CLI → HTTP API → Ollama Server → Scheduler → llama.cpp Runner 흐름을 확인했다.

flowchart TD
    CLIENT["curl / Ollama CLI"]
    SERVER["Ollama Server<br/>PID 70029 · 127.0.0.1:11434"]
    SCHEDULER["Scheduler"]
    RUNNER["llama-server<br/>PID 70246 · PPID 70029<br/>127.0.0.1:60191"]
    RESPONSE["HTTP 응답 반환"]
    CLIENT -->|"로컬 HTTP 요청"| SERVER
    SERVER -->|"요청 처리·모델 실행 조정"| SCHEDULER
    SCHEDULER -->|"기존 Runner 재사용 또는 새 프로세스 실행"| RUNNER
    RUNNER -->|"GGUF 모델 로딩·추론"| RESPONSE
  • ollama list는 등록된 모델을 확인한다. 실제로 gemma3-local:latest가 2.5 GB로 표시되었다.
  • 최초 ollama ps는 빈 목록이었다. 등록되어 있어도 메모리에 실행용으로 로드된 것은 아니었다.
  • POST /api/chat 요청 후 ollama ps에 해당 모델이 2.9 GB / 100% GPU / CONTEXT 32768 / RUNNER llamacpp로 표시되었다. 여기서 100% GPU는 GPU 이용률이 아닌 모델 배치 상태로 해석해야 한다. 2.5 GB와 2.9 GB의 차이를 특정 메모리 항목 하나의 크기로 단정할 수는 없다.
  • ps -axo pid,ppid,command로 확인한 실제 부모-자식 프로세스 관계는 ollama serve(PID 70029) → llama-server(PID 70246, PPID 70029)였다.
  • Runner 실행 명령에서 --model .../.ollama/models/blobs/sha256-ee91..., --port 60191, -c 32768, -np 1, --cache-type-k q8_0, --cache-type-v q8_0, --flash-attn on을 확인했다. 서버의 환경 설정이 Runner 실행 옵션으로 반영되었다.

관련 소스코드 (Ollama v0.40.9)

1-2. Cold Start 단일 요청 — 명령어와 결과

검증 환경과 전제 조건

  • Ollama v0.40.9, 모델 gemma3-local:latest (Gemma 3 4B Q4_0 GGUF)
  • ollama serve는 미리 실행하고, ollama stop gemma3-local로 Runner를 종료한 상태
  • 요청 직전 ollama ps가 빈 목록임을 확인한 Cold Start
  • 요청 JSON: stream: false, keep_alive: "10m"
  • 측정 중 다른 ollama run 또는 HTTP 추론 요청 실행하지 않음

재현 명령어

ollama ps
date '+START: %Y-%m-%d %H:%M:%S %z'
curl -sS \
  -w '\nHTTP_STATUS=%{http_code} CURL_TOTAL=%{time_total}s\n' \
  -o /tmp/ollama-chat-verify.json \
  http://127.0.0.1:11434/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemma3-local",
    "messages": [
      {
        "role": "user",
        "content": "Ollama 서버란 무엇인가?"
      }
    ],
    "stream": false,
    "keep_alive": "10m"
  }'
date '+END: %Y-%m-%d %H:%M:%S %z'
python3 -m json.tool /tmp/ollama-chat-verify.json
ollama ps

단일 요청 실측 결과 (2026-10-24 KST)

START: 2026-10-24 00:07:51 +0900
HTTP_STATUS=200 CURL_TOTAL=18.890085s
END: 2026-10-24 00:08:10 +0900
측정 항목 API 응답 같은 요청의 Runner 로그
입력 토큰 16 16
생성 토큰 765 765
프롬프트 평가 시간 90.170 ms 90.17 ms
토큰 생성 시간 18,268.082 ms 18,268.08 ms
내부 전체 처리 시간 18,889.107 ms (total_duration) 18,358.25 ms (prompt eval + eval)
별도 모델 로딩 시간 528.219 ms (load_duration) Runner 시작 약 0.50초

API 원본 성능 필드

{
  "done": true,
  "done_reason": "stop",
  "total_duration": 18889106583,
  "load_duration": 528218500,
  "prompt_eval_count": 16,
  "prompt_eval_cached_count": 0,
  "prompt_eval_duration": 90170000,
  "eval_count": 765,
  "eval_duration": 18268082000
}

동일 요청의 Runner 성능 로그

slot print_timing: prompt eval time = 90.17 ms / 16 tokens (177.44 tokens per second)
slot print_timing:        eval time = 18268.08 ms / 765 tokens (41.82 tokens per second)
slot print_timing:       total time = 18358.25 ms / 781 tokens
[GIN] 2026/10/24 - 00:08:10 | 200 | 18.88s | 127.0.0.1 | POST "/api/chat"

해석

  1. 토큰 수와 평가 시간이 API와 Runner 로그에서 일치한다. 요청 시작·종료 시각 및 /api/chat의 HTTP 200도 대응하므로 같은 요청의 결과로 교차 검증했다.
  2. total_duration(약 18.889초)은 모델 로딩 및 추론을 포함한 서버 측 전체 요청 처리 시간이다. Runner의 total time(약 18.358초)은 내부 Prefill+Decode 시간으로, 서로 측정 범위가 다르다.
  3. load_duration(약 0.528초) + prompt_eval_duration(약 0.090초) + eval_duration(약 18.268초) = 약 18.886초로, 서버 총 소요 시간과 매우 가깝다. 차이는 약 2.6ms이다.
  4. 클라이언트 curl 시간(18.890초)과 서버 total_duration(18.889초)의 차이도 약 1ms로, 같은 로컬 요청임을 뒷받침한다. 단, 측정 범위와 시스템 상태에 따라 차이는 달라진다.
  5. 이 요청에서는 765개의 출력 토큰을 생성하여 대부분의 시간이 Decode에 사용됐다. 이 결과만으로 다른 입력·출력 길이나 다른 모델에서 동일한 성능을 보장하지 않는다.

이번 요청에서 확인된 모델 로딩 로그

  • 00:07:51에 llama-server 자식 프로세스 실행 요청, 00:07:52에 모델 로딩 완료
  • offloaded 35/35 layers to GPU: 모델 레이어 전체를 GPU로 오프로딩한 것으로 보고됨
  • n_ctx = 32768, flash_attn = enabled
  • KV Cache: non-SWA 340.00 MiB + SWA 123.25 MiB (총 약 463.25 MiB, Runner가 보고한 버퍼 용량)
  • 이는 실행 시 할당·예약하는 구성 요소에 관한 추가 관찰이며, 운영체제 전체의 실제 메모리 점유량을 재는 실험과는 구분한다.

1-3. Runner 종료 — 명령어와 결과

ollama stop gemma3-local
ollama ps
ps -axo pid,ppid,command | grep -E '[o]llama|[l]lama-server'

실제 결과:

# ollama ps
NAME    ID    SIZE    PROCESSOR    CONTEXT    RUNNER    UNTIL

# ps
70029 28485 ollama serve

검증 결과: ollama stop gemma3-local 이후 로드된 모델 목록은 비었고, llama-server 자식 프로세스는 사라졌으며, 부모 ollama serve 프로세스(PID 70029)는 계속 실행되었다.

따라서 서버 생명주기와 모델 Runner 생명주기가 별도로 관리됨을 직접 확인했다.

심화 2단계 — LLM 추론 과정과 로그 읽기

명령어·결과의 연결: 이 단계에서는 심화 1단계의 Cold Start 단일 요청(16 입력 / 765 출력 토큰) 로그를 바탕으로 Prefill과 Decode를 해석했다.

학습 목표: 사용자 입력이 숫자 연산으로 변환되는 과정과 LLM이 다음 토큰을 반복해서 생성하는 원리를 이해한다.

이번에는 핵심 원리를 간단한 행렬 예제로 학습했으며, 실제 Gemma 3의 모든 내부 연산을 직접 재구현한 것은 아니다.

2-1. 전체 처리 흐름

flowchart TD
    INPUT["사용자 입력 문자열"]
    TOKENIZER["Tokenizer<br/>문자열 → Token ID 시퀀스"]
    EMBEDDING["Embedding<br/>Token ID에 대응하는 벡터 조회"]
    TRANSFORMER["Transformer Layers<br/>Attention · Feed-Forward 등"]
    LOGITS["출력 투영<br/>Vocabulary의 토큰별 Logits 계산"]
    SAMPLE["Sampling<br/>생성 설정에 따라 다음 Token ID 선택"]
    STOP{"종료 토큰 또는<br/>생성 종료 조건 충족?"}
    APPEND["선택한 토큰을 문맥에 추가<br/>KV Cache를 활용해 다음 토큰 처리"]
    OUTPUT["출력 완료"]
    INPUT --> TOKENIZER --> EMBEDDING --> TRANSFORMER
    TRANSFORMER --> LOGITS --> SAMPLE --> STOP
    STOP -->|"예"| OUTPUT
    STOP -->|"아니오"| APPEND
    APPEND --> EMBEDDING

LLM은 문자열을 직접 행렬에 넣지 않고, Tokenizer로 정수 ID를 만든 뒤 Embedding을 조회하여 연속적인 숫자 벡터를 얻는다.

Token ID의 숫자 자체는 의미의 크기나 중요도를 나타내지 않는다.

대화 API에서는 사용자 텍스트뿐 아니라 Chat Template과 특수 토큰이 함께 처리될 수 있다.

2-2. 행렬 곱셈과 다음 토큰 점수

Transformer에서는 입력 표현과 학습된 가중치 행렬을 곱해 새로운 표현을 계산한다.

가장 단순한 선형 변환은 \(y=xW\)로 표현된다. 실제 연산에는 Attention, 정규화, 비선형 활성화, 잔차 연결 등이 추가된다.

다음은 실제 모델의 값이 아닌 계산 원리 설명용 가상 예시다.

입력 벡터 x = [2, 1]

가중치 W = [[ 1, 0, -1],
            [ 0, 1,  0]]

xW = [2, 1, -2]   # 토큰 후보 세 개에 대한 Logits라고 가정

Logit은 확률이 아닌 점수다. 이 가상의 세 점수에 Softmax를 적용하면 약 [72.1%, 26.5%, 1.3%]가 된다.

실제 모델은 전체 어휘에 대한 점수를 산출하며, Sampling 설정에 따라 다음 토큰을 선택한다.

2-3. Self-Attention: Q, K, V는 무엇인가?

Self-Attention은 현재 토큰이 이전 문맥의 어떤 정보를 얼마나 참고할지 계산하는 연산이다.

  • Query(Q): 참고할 정보를 찾을 때 사용하는 벡터
  • Key(K): Query와의 관련성 점수를 계산할 때 사용하는 벡터
  • Value(V): 계산된 가중치에 따라 실제로 결합할 정보 벡터

입력 표현 \(X\)와 학습된 행렬을 곱해 \(Q=XW_Q\), \(K=XW_K\), \(V=XW_V\)를 얻는다.

기본 Scaled Dot-Product Attention은 다음과 같이 쓸 수 있다.

\[ \operatorname{Attention}(Q,K,V)=\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}+M\right)V \]

여기서 \(M\)은 미래 토큰을 참조하지 못하게 하는 Causal Mask 등을 나타낸다.

가상 2차원 실습 예시

토큰 A: [1, 0] / 토큰 B: [0, 1]
이해를 위해 W_Q, W_K, W_V 모두 단위행렬이라 가정

Q_B · K_A = 0
Q_B · K_B = 1
차원 2로 스케일: [0, 1/sqrt(2)] ≈ [0, 0.707]
Softmax → [0.330, 0.670]

토큰 B의 Attention 출력
= 0.330 × V_A + 0.670 × V_B
= [0.330, 0.670]

이 값은 앞선 토큰과 현재 토큰의 정보를 결합한 숫자 표현이다.

바로 다음 출력 토큰을 뜻하지는 않으며, Transformer의 나머지 연산을 거쳐 최종 Logits를 계산해야 한다.

2-4. KV Cache: 어떤 계산을 재사용하는가?

자기회귀 생성에서는 다음 토큰을 순서대로 생성한다. 과거 토큰의 Key와 Value를 매번 새로 계산하면 중복 연산이 많아진다.

  • KV Cache 사용 시: 과거 토큰의 K, V는 저장해 재사용한다.
  • 새 토큰 처리 시: 새로운 토큰의 Q, K, V 및 필요한 Attention과 나머지 Transformer 연산은 수행한다.
  • 주의: KV Cache가 모든 Attention 연산을 생략하는 것은 아니다. Sliding Window Attention 등 모델 구조에 따라 캐시 유지 및 재사용 범위에도 제약이 있다.

Gemma 3는 Grouped-Query Attention(GQA) 구조를 사용하며, 이번 GGUF 로그에서 Query Head 8개, KV Head 4개, Sliding Window 1024토큰을 확인했다.

2-5. Sampling: 점수에서 다음 토큰으로

Transformer의 최종 표현을 출력 계층에 전달하면 Vocabulary의 토큰별 Logits가 계산된다.

이후 생성 정책에 따라 토큰을 선택한다.

같은 프롬프트라도 Sampling 설정에 따라 출력이 달라질 수 있다.

이번 실습의 Runner 로그에는 top_k=40, top_p=0.900, temp=0.800이 표시되었다.

옵션 핵심 의미
temperature 토큰 점수 분포의 날카로움을 조절
top_k 상위 점수 후보를 최대 K개로 제한
top_p 누적 확률이 임계값에 도달하는 상위 후보 집합을 고려

선택한 토큰은 문맥에 추가되고, 다음 토큰을 예측하는 과정을 종료 조건까지 반복한다.

이를 자기회귀 생성(Autoregressive Generation)이라고 한다.

2-6. Prefill과 Decode: 실제 실습 로그에 연결

  • Prefill: 입력 프롬프트 토큰들을 처리하고 이후 생성에 사용할 KV Cache 상태를 준비한다. 여러 입력 토큰의 연산을 병렬화하기에 유리하다.
  • Decode: 이전 계산 결과와 KV Cache를 이용해 새로운 토큰을 차례로 생성한다. 토큰 생성 단계는 앞서 생성한 결과에 의존한다.

심화 1단계에서 동일한 단일 HTTP 요청과 Runner 로그를 교차 검증한 실제 결과는 다음과 같다.

구분 처리한 토큰 시간 처리량
Prefill 16 90.17ms 177.44 tokens/s
Decode 765 18,268.08ms 41.82 tokens/s

이 요청에서는 출력 토큰 수가 765개로 많아 Decode 시간이 대부분을 차지했다.

위 수치는 해당 단일 요청의 측정값이며, 다른 프롬프트 길이나 모델의 일반적인 성능을 대표하지 않는다.

이번 2단계에서는 로그를 해석했으며 Attention 내부의 실제 GPU 연산을 별도로 프로파일링하지는 않았다.

실제 로딩 로그에서 확인한 Gemma 3 4B 정보

항목 값
Vocabulary (n_vocab) 262,208
Hidden / Embedding Dimension (n_embd) 2,560
Transformer Layer (n_layer) 34
Query Head (n_head) 8
KV Head (n_head_kv) 4
Sliding Window (n_swa) 1,024

반드시 이해할 핵심: 텍스트 → Token ID → Embedding → Transformer 행렬 연산·Attention → Logits → Sampling → 다음 토큰의 흐름이다. KV Cache는 과거 K/V를 재사용하며, Prefill과 Decode의 실행 특성은 다르다.

심화 3단계 — Context 변경에 따른 메모리 비교

학습 목표: GGUF 파일의 저장 크기와 추론 중 메모리 사용량을 구분하고, Apple Silicon의 Unified Memory 구조와 Context·KV Cache 간의 관계를 이해한다.

3-1. Apple Unified Memory

Apple Silicon은 CPU와 GPU가 같은 물리적 시스템 메모리 풀을 공유한다.

별도 GPU VRAM이 존재하는 전형적인 Discrete GPU 시스템과는 다르다.

다만 같은 메모리를 공유한다는 사실과 각 리소스에 대한 CPU·GPU 접근 권한은 다른 문제다.

Metal의 shared 저장 모드에서는 CPU·GPU가 모두 접근할 수 있지만 private 리소스는 GPU가 직접 접근하도록 설정된다.

이번 실행 환경은 Apple M5, 시스템 메모리 32.0 GiB, Metal GPU, Ollama v0.40.9다.

3-2. GGUF 파일 크기와 런타임 메모리

모델 GGUF 로그는 file type=Q4_0, file size=2.35 GiB, model params=3.88 B를 보고했다.

ollama list에서 보이는 약 2.5 GB는 디스크 저장 크기에 가까운 값이며, 런타임에는 모델 가중치에 더해 KV Cache, GPU·CPU Compute Buffer, 기타 실행용 자원이 필요하다.

또한 ollama ps의 100% GPU는 모델 배치 상태를 보여주는 것이지, GPU 사용률 100%를 뜻하지 않는다.

모델이 GPU에 배치되어 있어도 CPU는 HTTP 요청 처리, 실행 관리와 기타 연산에 참여한다.

중요한 측정 구분:

  • ollama ps SIZE: Ollama가 출력하는 로드된 모델 크기
  • llama-server의 MTL0 KV buffer size·compute buffer size: Runner 내부의 개별 버퍼 할당/예약 정보
  • macOS의 전체 물리 RAM 점유량: 운영체제 도구로 별도 측정해야 할 지표

위 세 종류의 값을 그대로 합산하거나 동일한 측정값처럼 비교하면 안 된다.

파일 매핑과 Unified Memory의 공유 특성, 버퍼 예약량 등이 다르기 때문이다.

3-3. Context Window와 KV Cache

Context Window가 커지면 모델이 다룰 수 있는 토큰 범위가 증가하지만, 일반적으로 더 큰 KV Cache 등의 메모리 확보가 필요할 수 있다.

모델 가중치 크기는 Context 설정을 바꿨다는 이유만으로 바뀌지 않는다.

Gemma 3는 Global Attention 계열의 Non-SWA와 Sliding Window Attention(SWA)을 함께 사용한다.

따라서 KV Cache가 반드시 Context 길이에 비례해서 모두 증가하는 것은 아니다.

이 관계는 이 단계의 Context 비교 실습에서 동일 모델·동일 요청·동일 Runner 옵션(-b 512 -ub 512)으로 비교하여 확인했다.

3-4. Context 4096 / 32768 — 실행 명령어와 결과

목적: 다른 모델·양자화·질문 내용은 유지한 채 num_ctx만 변경해 KV Cache 및 Compute Buffer 메모리 할당의 차이를 검증한다.

실행 환경: Apple M5, 32.0 GiB Unified Memory, Ollama v0.40.9, gemma3-local:latest (Gemma 3 4B Q4_0 GGUF), OLLAMA_FLASH_ATTENTION=1, OLLAMA_KV_CACHE_TYPE=q8_0, OLLAMA_NO_CLOUD=1.

각 요청 전 ollama stop gemma3-local로 Runner를 종료했고 num_predict=32, stream=false, keep_alive=10m을 동일하게 적용했다.

실제 로그에서 두 Runner 모두 -b 512 -ub 512였다.

실제 실행 명령어 원문 (사용자 CLI 입력)

for ctx in 4096 32768; do
  ollama stop gemma3-local >/dev/null 2>&1

  printf '\n=== CONTEXT: %s ===\n' "$ctx"

  curl -sS http://127.0.0.1:11434/api/chat \
    -H 'Content-Type: application/json' \
    -d "{
      \"model\": \"gemma3-local\",
      \"messages\": [
        {
          \"role\": \"user\",
          \"content\": \"Ollama를 한 문장으로 설명해 줘.\"
        }
      ],
      \"stream\": false,
      \"keep_alive\": \"10m\",
      \"options\": {
        \"num_ctx\": $ctx,
        \"num_predict\": 32
      }
    }" | python3 -c 'import json,sys; d=json.load(sys.stdin); print("done:",d.get("done"),"eval_count:",d.get("eval_count"))'

  ollama ps
done

실제 CLI 결과 원문 (각 Context 구분, 헤더·ID·UNTIL 포함)

=== CONTEXT: 4096 ===
done: True eval_count: 32
NAME                   ID              SIZE      PROCESSOR    CONTEXT    RUNNER      UNTIL
gemma3-local:latest    048ab3b6a24f    2.7 GB    100% GPU     4096       llamacpp    9 minutes from now

=== CONTEXT: 32768 ===
done: True eval_count: 32
NAME                   ID              SIZE      PROCESSOR    CONTEXT    RUNNER      UNTIL
gemma3-local:latest    048ab3b6a24f    2.8 GB    100% GPU     32768      llamacpp    9 minutes from now

동일 실험의 실제 Runner 로그 비교

항목 Context 4,096 Context 32,768 해석
ollama ps SIZE 2.7 GB 2.8 GB Ollama의 표시 크기 약 +0.1 GB
GPU 모델 버퍼 2,402.82 MiB 2,402.82 MiB 모델 자체는 동일
Non-SWA KV Cache 42.50 MiB 340.00 MiB 캐시 셀 4,096 → 32,768; 정확히 8배
SWA KV Cache 92.44 MiB 92.44 MiB 캐시 셀 두 실행 모두 1,536
KV Cache 합계 134.94 MiB 432.44 MiB +297.50 MiB, 약 3.20배
GPU Compute Buffer 86.05 MiB 198.46 MiB +112.41 MiB
CPU Compute Buffer 26.06 MiB 54.06 MiB +28.00 MiB
CPU Mapped 모델 버퍼 680.17 MiB 680.17 MiB 동일
Runner 내부 Prefill 시간 208.55ms / 21토큰 123.19ms / 21토큰 1회 측정, Context 자체의 속도 효과로 단정 불가
Runner 내부 Decode 시간 721.03ms / 32토큰 750.09ms / 32토큰 1회 측정, 변동 가능

해석 및 검증 결론

  1. num_ctx를 8배로 늘려도 모델 가중치 버퍼 크기는 유지됐다.
  2. 이번 llama.cpp Runner의 Non-SWA KV Cache는 캐시 셀 수와 버퍼 크기가 함께 정확히 8배 증가했다.
  3. SWA KV Cache는 두 설정 모두 1,536개 셀·92.44 MiB로 동일했다. Global 계열과 Sliding Window 계열의 메모리 증가 특성이 다른 것을 직접 확인했다.
  4. KV Cache 합계는 134.94 → 432.44 MiB로 297.50 MiB 증가했다. 여기에 GPU Compute Buffer 증가분 112.41 MiB가 더해진다.
  5. ollama ps SIZE는 2.7 → 2.8 GB로만 달라졌다. 이 표시 크기와 Runner 개별 버퍼 합산값은 서로 집계 기준이 같지 않으므로 차이만으로 정확한 macOS 물리 RAM 점유량을 역산할 수 없다.
  6. 동일한 32토큰 생성을 성공적으로 수행했으나, 각 설정당 1회 실행이므로 속도에 대한 일반적인 성능 결론은 내리지 않는다. 본 실습은 실행시간 벤치마크가 아니라 메모리 할당 구성 비교를 위한 것이다.

다른 요청의 로그와 혼동하지 않기: 앞선 Context 32,768 실험에서는 SWA 캐시가 123.25 MiB / 2,048 cells, GPU Compute Buffer가 261.08 MiB로 기록됐다. 이번 비교 실험에서는 동일 Context 32,768이라도 SWA 캐시는 92.44 MiB / 1,536 cells, GPU Compute Buffer는 198.46 MiB였다. 앞선 실행은 Runner 옵션이 -b 1024 -ub 1024였고 이번 비교는 -b 512 -ub 512였다. 따라서 두 실행 간 차이를 Context 설정만의 효과로 해석해서는 안 된다. 이번 비교는 두 Runner의 배치 설정이 동일한 기록만 사용한다.

추가 학습 범위: 더 정확한 시스템 물리 메모리 사용량 분석은 vmmap, Activity Monitor, Metal 메모리 지표 등을 함께 수집해야 한다. 이번 실습에서는 운영체제 관점의 프로세스별 실제 상주 메모리까지 측정했다고 주장하지 않는다.

3-5. 실측 그래프

아래는 이 단계의 비교 표와 같은 요청 A/B의 수치만 사용한 막대그래프다.

KV Cache·Compute는 MiB, ollama ps SIZE는 GB로 축을 분리하고 0부터 표시했다. KV 합계만 계산값이다.

기존 상세 표와 해석은 이 단계의 비교 표와 해석을 참조한다.

Context별 KV Cache 및 Compute Buffer 비교

GPU 모델 버퍼 비교

Ollama 표시 크기 비교

학습 정리

  • Ollama 서버는 CLI 명령과 별개로 계속 실행되는 프로세스이며, CLI는 로컬 HTTP API를 통해 요청을 전달한다.
  • Ollama 프로그램 설치, 서버 실행, 모델 파일 등록, 실제 추론은 서로 다른 작업이다.
  • GGUF 파일을 직접 준비한 뒤 Modelfile과 ollama create로 외부 레지스트리의 모델 패키지 다운로드 없이 로컬 모델을 등록할 수 있었다.
  • 로컬 서버 API의 정상 응답과 실제 LLM 응답을 모두 확인했다.
  • GPU 탐지와 실제 GPU 사용률, 모델 메모리 점유량 및 세부 추론 동작은 구분해서 검증해야 한다.
  • LLM의 다음 토큰 예측은 텍스트를 Token ID와 Embedding으로 변환한 뒤 Transformer 연산을 거쳐 Logits를 계산하고 Sampling으로 토큰을 선택하는 과정이다. KV Cache는 과거 토큰의 K/V 중복 계산을 줄인다.
  • Context 4,096 → 32,768 실습에서 Non-SWA KV Cache는 42.50 → 340.00 MiB로 8배 증가했으나 SWA Cache는 92.44 MiB로 동일했다. 모델 버퍼는 일정했으며, GPU Compute Buffer도 증가했다. 이 값들은 런타임 버퍼 할당량이지 OS 전체 물리 RAM 점유량은 아니다.

참고 자료