이번 포스팅은 로컬 LLM 구축기의 첫 번째 시리즈입니다. 용도가 애매했던 미니 PC를 중고로 내놓으려다가 로컬 LLM 구동 가능성을 확인한 후 구축 → 테스트 → 버그 발견 → 오픈소스 기여까지의 내용을 연재를 통해 소개하고자 합니다. 마이너 한 하드웨어 조합의 한계 때문에 오히려 배울 수 있었던 점이 많았기에 이를 공유하고자 합니다.
1. 신샤드, 그는 뭄 인가?
출근길 지하철에서 알고리즘에 뜬 동영상 중 하나가 눈길을 끌었습니다. Kubernetes를 공부해 본 분이라면 한 번쯤은 보셨을 그분... 바로 뭄샤드 센세입니다. 게다가 자동 더빙(좀 멍청한 말투이긴 하지만)이 들어가 있어, 라디오 처럼 틀어놓고 들을 수 있어 바로 시청을 시작했습니다. 혹시 AI 인프라에 관심이 있는 분들이라면 꼭 한번 보시면 좋겠습니다. 강력 추천하는 영상입니다.

사실 AI 인프라에는 개념이 거의 없었기 때문에 왜 GPU가 필요한지, 왜 고 대역폭 메모리가 필요한지 잘 모르고 있었습니다. 그냥 막연하게 '모델의 추론이라는 게 단순한 행렬 곱셈이고 이 행렬의 차원이 어마어마하게 커서 CPU보다는 더 많은 프로세서를 가지고 있는 GPU가 유리하다.' 정도로만 알고 있었습니다. 하지만 위 영상을 보고 그 내용을 쉽게 알 수 있었는데, 요지는 토큰 하나를 생성할 때마다 모델 전체를 프로세서(그게 CPU 던 GPU 던 간에)가 읽어야 하는데 이를 읽는 속도는 메모리와 프로세서 사이의 대역폭이 결정한다는 것입니다. 그 외에도 여러 가지 개념을 쉽게 설명해 놓았으니 관심 있으시면 한번 꼭 보시면 좋을 것 같습니다.
1.1 통합 메모리 아키텍처

집에 그 용도가 애매한 미니 PC가 하나 놀고 있었습니다. GMKtec K8 Plus라는 모델로 AMD Ryzen 7 8845HS 프로세서를 탑재하고 있고 AMD Radeon 780M 내장 GPU를 내장하고 있습니다. 메모리는 64GB에 2TB SSD를 장착하고 있어서 저장공간도 매우 넉넉한 편으로 구매 당시에 75만 원 정도 주었던 것 같습니다. 메모리 가격이 폭등기 전 의도하지 않게 막차를 탄 셈입니다.
어쨌든, '이 녀석으로 어떻게 AI를 돌린다는거냐?' 라고 질문할 수 있습니다. 그 답은 통합 메모리 아키텍처에 있습니다. 시스템 메모리 64GB 중 일부를 GPU가 점유할 수 있도록 설정이 가능해지면서 수십 기가 수준의 모델을 올릴 수가 있는 것입니다. 일반 소비자용 GPU의 VRAM이 24GB면 높은 수준인 것을 감안하면 매우 넉넉한 용량입니다.
그러나, 한계도 명확합니다. DDR5 듀얼 채널의 시스템 메모리 대역폭은 이론상 약 89.6GB/s 수준이고, 실 사용 수준은 70GB/s - 80GB/s 정도가 됩니다. 그럼 이론상 20GB 모델을 사용한다 가정 시 초당 3.5 tok/s - 4 tok/s로 사실상 사용이 불가능한 수준이 됩니다. 하지만 아직 실망하기에는 이릅니다.
1.2 MoE 아키텍처

여기서 또 사람들은 새로운 발상을 합니다. '토큰 하나를 생성하는데 모델 전체 파라미터를 사용하는 것이 아닌 그중 일부만을 사용하면 더 효율적이지 않을까?' 바로, MoE(Mixture of Experts)의 등장입니다. 이 부분에서 저의 이해는, 매 토큰 생성마다 라우터가 전체 파라미터 중 적합한 파라미터만을 선정하여 활성화시킨다입니다. 그러면 실제 프로세서가 읽어야 할 정보는 수 GB 수준으로 낮아지기 때문에 제법 쓸만한 토큰 생성 속도가 나오게 됩니다. 드디어 로컬 LLM 구현 가능성이 열렸습니다.
2. 기반 준비
이제 가능성이 열렸으니 본격적으로 구축을 시작해 봅시다. 홈랩을 하다보면 하드웨어 최적화가 중요하다는 것을 항상 느낍니다. 제한된 환경에서 최대한의 성능을 끌어내어 마지막 한 방울까지 쥐어짜야 합니다.
2.1 BIOS 세팅
| 항목 | 경로 | 설정값 | 이유 |
| UMA Frame Buffer Size | Advanced → AMD CBS → NBIO Common Options → GFX Configuration | 1GB(선택 가능한 최소값) | - 이 값은 BIOS가 부팅 시 고정 예약하는 작은 VRAM 용량일 뿐이며, 실제 대용량 GPU 메모리 풀은 GTT가 담당 - 크게 잡을수록 그만큼 시스템 RAM이 OS에서 안 보이게 영구 손실되므로 최소로 설정 |
| SVM Mode | Advanced → CPU Configuration | Disabled | - AMD-V 하드웨어 가상화 토글로 Docker를 사용하는데는 하드웨어 가상화가 필요 없어 끔 |
| IOMMU | Advanced → AMD CBS → NBIO Common Options | Disabled | - IOMMU 오버헤드 제거 및 GTT DRA를 위해 끔 |
| 전력 모드 | Main → Power | Performance | - 추론 성능을 최대로 끌어내기 위해 내장 GPU/CPU 전력량을 높여 클력이 낮은 전력 모드에 묶이는 것을 방지 |
2.2 OS 선택
OS는 취향 차리가 생각합니다. 저는 Linux 배포판 중 Ubuntu를 선호하는 편이고, 이번에 실험적으로 Ubuntu 26.04 LTS를 사용하기로 했습니다. Linux 커널 버전 7을 사용하며 최신 RDNA3을 통해 780M 내장 GPU를 지원이 보장됩니다.
2.3 커널 파라미터 설정
OS 설치가 끝나고 나면 텍스트 편집기를 활용해 /etc/default/grub 파일을 열고 아래 값을 입력합니다. GTT 확장과 IOMMU 비활성화입니다.
GRUB_CMDLINE_LINUX_DEFAULT="amd_iommu=off ttm.pages_limit=12582912"
일력 후 이를 활성화하기 위해서는 grup 업데이트와 재부팅이 필요합니다. 그 후 검증까지 아래 명령어를 통해 수행합니다.
# grup 업데이트 및 재부팅
sudo update-grub
sudo reboot
# 재부팅 후 검증
sudo dmesg | grep amdgpu, sudo dmesg | grep -i iommu, sudo dmesg | grep -i amd-vi
# 출력 확인
amdgpu 0000:c6:00.0: 1024M of VRAM memory ready # UMA Frame Buffer Size 1GB로 설정
amdgpu 0000:c6:00.0: 49152M of GTT memory ready. # GTT용 48GB 확인
Command line: ... amd_iommu=off ttm.pages_limit=12582912 ... # IOMMU Off 확인
GTT와 IOMMU에 대한 설정은 아래 더 보기를 참고해 주세요.
GTT(Graphic Translation Table)
앞서 이야기했지만 AMD 내장 GPU는 시스템 메모리를 공유합니다. 앞선 BIOS 설정에서는 이 시스템 메모리 중 VRAM으로 사용할 예약 메모리를 최소로 하였습니다. 이유는 공유 메모리 크기를 최대한 확보하고 GTT를 통해서 VRAM을 지정해 주기 위함입니다. 여기서 GTT란 Graphics Translation Table의 약어로 물리 메모리를 GPU가 사용할 수 있도록 동적 가상 주소 공간에 매핑하는 메모리 관리 기술입니다. 이를 통해, 내장 GPU가 시스템 메모리의 일부를 VRAM인 것처럼 사용할 수 있게 됩니다.
ttm.pages_limit=12582912 설정은 64GB의 시스템 메모리 중 48GB를 GPU 메모리로 할당한다는 의미입니다.(참고: 1GB = 262144 페이지) 나머지 16GB는 OS 및 기타 용도로 사용하게 하였습니다.
IOMMU(Input-Output Memory Management Unit)
IOMMU는 컴퓨터 내부의 NIC, GPU, 스토리지 컨트롤러와 같은 장치(Device)가 발행하는 메모리 주소를 실제 물리 주소로 번역하고 권한을 검사하는 하드웨어 유닛입니다. AMD 구현체 이름은 AMD-vi, 인텔은 VT-d입니다.
이런 장치들은 DMA(Direct Memory Access)를 사용하여 CPU를 거치지 않고 메모리에 직접 읽기와 쓰기를 수행했습니다. 역사적으로 물리 주소를 그대로 사용했던 것이죠. 그래서 장치 하나가 오작동하거나 악의적인 커널 메모리를 포함하고 있으면 RAM을 어디든 건드릴 수 있었습니다. 장치가 직접 메모리에 접근하면서 발생하는 문제점을 해결하기 위해 IOMMU라는 유닛이 중계 역할을 하게 됩니다. 실제 물리 메모리에 대한 자체 페이지 테이블을 가지고 있어 장치가 발행한 DMA 주소를 받아 실제 물리 주소로 변환해 주는 역할을 하는 것입니다. 이를 통해 장치의 이상 행동이을 감지 하여 권한 위반이나 오작동을 차단하게 됩니다.
문제는 이 주소 중계에 비용이 발생한다는 겁니다. IOMMU를 통하게 되므로 map/unmap 자체에 CPU 오버헤드가 발생하기도 하고 매핑이 어긋나면 페이지 테이블 워크가 발생하기도 합니다. 이번 포스팅의 경우에는 내장 GPU가 시스템 메모리의 GTT 영역을 대상으로 LLM 호출이 있을 때마다 대량 DMA를 하게 되는 구조입니다. 매 토큰 생성마다 시스템 메모리를 읽어 와야 하고, 이때 IOMMU의 번역을 거쳐야 하기 때문에 안 그래도 낮은 메모리 대역폭에 병목이 하나 더 추가되는 구조입니다.
단, 분명히 문제도 있습니다. 장치들에 악의적인 접근이나 오류에 취약해진다는 보안적인 부분으로, 누군가 악의적으로 이 미니 PC에 접근한다면 문제가 생길 수 있는 구조입니다. 그래서 이 미니 PC는 LLM 추론 이외에는 다른 용도로 사용하지 않고, 네트워크 접근도 최대한 통제하는 방향을 택했습니다. 실제 적용에는 이 trade-off를 잘 따져 보면 좋겠습니다.
2.4 Vulkan 드라이버 및 Docker 설치
Ollama를 사용하여 LLM을 돌리기 위해서는 GPU와 통신할 수 있도록 드라이버가 필요합니다. 이때 Vulkan 드라이버가 아주 중요한 역할을 합니다. 정확히는 Vulkan은 일종의 표준 명세일 뿐이고 이 표준을 AMD GPU용으로 구현한 것이 RADV인데, 자세한 내용은 다음 포스팅에 다뤄보도록 하겠습니다.
# Vulkan 드라이버 설치
sudo apt update
sudo apt install -y mesa-vulkan-drivers vulkan-tools
sudo usermod -aG render <사용자 명>
sudo usermod -aG video <사용자 명> # 로그아웃 후 재접속 필요 (그룹 변경은 새 세션부터 적용)
# Docker 설치
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker <사용자 명> # 로그아웃 후 재접속 필요
docker run hello-world # sudo 없이 정상 동작 확인
2.5 설정 최종 상태 확인
vendorID = 0x1002
deviceID = 0x1900
deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
deviceName = AMD Radeon 780M Graphics (RADV PHOENIX)
driverName = radv
3. Ollama 컨테이너 실행
Ollama 및 모델은 모두 컨테이너 기반으로 구동합니다. 이번 포스팅에서는 Docker Compose를 사용하였습니다.
#docker-compose.yml
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
devices:
- /dev/dri:/dev/dri
volumes:
- ollama:/root/.ollama
ports:
- "11434:11434"
environment:
- OLLAMA_VULKAN=1 # Vulkan 드라이버 사용
- OLLAMA_IGPU_ENABLE=1 # 내장 GPU 오프로딩 활성화
- OLLAMA_KEEP_ALIVE=-1 # 모델이 메모리에 상주
- OLLAMA_NO_CLOUD=true
volumes:
ollama:
# Docker compose 실행
docker compose up -d
docker compose logs -f ollama
# 최종 검증
msg="inference compute" id=0 library=Vulkan name=Vulkan0 description="AMD Radeon 780M Graphics (RADV PHOENIX)" type=iGPU total="59.0 GiB" available="48.9 GiB"
4. 모델 선택
모델을 선택하는 데 있어 중요하게 보았던 점은 아래 두 가지였습니다.
- 64GB 메모리에 올라갈 수 있는 크기인가?
- 70GB/s - 80GB/s 대역폭에 사용할 수 있는 수준이 되는가?
이러한 하드웨어 제약 사항에서 가장 적합한 모델의 형태는 MoE라는 판단은 앞에서도 한번 언급했습니다. 그래서 qwen3.6 35B-a3b을 최종 모델로 선정했습니다. 모델에 대한 자세한 설명도 다음에 해 보도록 하겠습니다.
# Ollama를 사용해 모델을 다운로드
docker compose exec ollama ollama pull qwen3.6:35b-a3b
# 모델 정보 확인
docker exec ollama ollama show qwen3.6:35b-a3b
Model
architecture qwen35moe # 모델 명
parameters 35.5B # 파라미터 수
context length 262144 # 컨텍스트 사이즈
embedding length 2048
quantization Q4_K_M # 4비트 양자화 모델
requires 0.30.0
Capabilities
completion
vision
tools
thinking
Projector
architecture clip
parameters 446.57M
embedding length 1152
dimensions 2048
Parameters
draft_num_predict 2
min_p 0
num_batch 512
presence_penalty 1.5
repeat_penalty 1
temperature 1
top_k 20
top_p 0.95
License
Apache License
Version 2.0, January 2004
...
그리고 대망의 첫 대화입니다.
docker compose exec ollama ollama run --verbose qwen3.6:35b-a3b "안녕, 너는 누구니? 자기소개를 부탁해."
Thinking...
Thinking Process:
1. **Analyze the user's input**: The user says "안녕, 너는 누구니? 자기소개를 부탁해
." (Hello, who are you? Please introduce yourself.). This is a direct question about
my identity and origins in Korean.
2. **Check instructions and capabilities**:
* I am Qwen, developed by Alibaba Group's Tongyi Lab.
* Tone: Empathetic, insightful, transparent, honest, helpful, clear, concise,
balancing warmth and intellectual honesty.
* Adapt language to match user (Korean).
3. **Drafting the response**:
* Greeting/Introduction: "안녕하세요! 저는 알리바바 그룹의 통이 연구소(Tongyi
Lab)에서 독자적으로 개발한 대형 언어 모델인 **Qwen**(통이치엔원)입니다." (Hello! I am
Qwen, a large language model independently developed by Alibaba Group's Tongyi Lab.)
* Elaborate on capabilities/identity: Keep it concise and helpful. "사용자의 질문
과 작업 요청을 다각도로 분석하고 심층적인 추론 및 문제 해결 능력을 바탕으로 명확하고 유
용한 답변을 드리는 것을 목표로 하고 있습니다." (I aim to provide clear and helpful
answers by analyzing user questions from various angles and providing in-depth
reasoning.)
* Offer help: "궁금한 점이 있거나 도움이 필요하신 부분이 있다면 언제든 편하게 말
씀해 주세요! 무엇을 도와드릴까요?" (If you have any questions or need help, feel free
to ask! How can I assist you today?)
4. **Review against guidelines**:
* Did I use the correct name? Yes, Qwen (통이치엔원).
* Is the tone appropriate? Yes, polite, warm, and helpful in Korean.
* Is it concise? Yes.
5. **Final Polish (Korean)**:
안녕하세요! 저는 알리바바 그룹의 통이 연구소(Tongyi Lab)에서 독자적으로 개발한 대형
언어 모델, **Qwen(통이치엔원)**입니다. 💙
복잡한 문제를 논리적으로 분석하고 다국어와 코딩을 포함한 다양한 작업을 원활하게 처
리하는 것을 목표로 합니다. 궁금한 점이 있거나 도움이 필요하신 부분이 있다면 언제든 편하
게 말씀해 주세요! 오늘 어떤 도움이 필요하신가요?
...done thinking.
안녕하세요! 저는 알리바바 그룹의 통이 연구소(Tongyi Lab)에서 독자적으로 개발한 대형 언
어 모델, **Qwen(통이치엔원)**입니다.
복잡한 문제를 논리적으로 분석하고 자연스러운 대화는 물론 코딩, 다국어 번역 등 다양한 작
업을 지원하기 위해 설계되었습니다.
궁금한 점이 있거나 도움이 필요하신 부분이 있다면 언제든 편하게 말씀해 주세요! 오늘 어떤
도움을 드릴까요?
total duration: 18.661490367s
load duration: 401.678µs
prompt eval count: 24 token(s)
prompt eval cached: 20 token(s)
prompt eval duration: 127.582ms
prompt eval rate: 31.35 tokens/s
eval count: 607 token(s)
eval duration: 18.463421s
eval rate: 32.88 tokens/s
약간의 생각과 함께 약 18초를 조금 넘는 시간에 응답을 하였습니다. 토큰 생성 속도도 약 33 tokens/s에 근접하여 나름 괜찮다고 느꼈습니다. 하지만, 이런 단발성 대화의 성능과 실제 Agent에서 사용할 때는 그 양상이 완전히 달라진다는 것을 이때는 몰랐습니다. 실제 Kagent에 LLM 백앤드로 붙여서 사용하면서 겪은 여러 문제점과 원인 추적, 그리고 그러면서 배운 LLM의 여러 개념들에 대해서 이어지는 시리즈 포스팅으로 계속 진행해 보도록 하겠습니다.
긴 글 일어주셔서 감사합니다.
'Study > CS & AI' 카테고리의 다른 글
| [Cilium Study] 8주차 - 네트워크를 넘어 커널까지: Cilium Security와 Tetragon (0) | 2025.09.07 |
|---|---|
| [Cilium Study] 7주차 - 대규모 클러스터를 위한 Kubernetes와 Cilium 성능 분석 및 튜닝 (1) | 2025.08.30 |
| [Cilium Study] 6주차 - Sidecar를 넘어, eBPF로 구현하는 서비스 메시 (5) | 2025.08.23 |
| [Cilium Study] 5주차 - BGP와 ClusterMesh로 클러스터 경계 넘기 (10) | 2025.08.17 |
| [Cilium Study] 4주차 - Service와 LoadBalancer로 세상과 소통하기 (7) | 2025.08.09 |
































































