1. 도입: GPU 없이 LLM을 돌린다는 것
얼마 전 Hacker News에 올라온 글 하나가 인프라 하는 사람들 사이에서 돌았다. 제목이 "Running Gemma 4 26B at 5 tokens/sec on a 13-year-old Xeon with no GPU"다. 요약하면 이렇다. 2013년산 HP StoreVirtual 스토리지 박스(듀얼 Ivy Bridge Xeon E5-2690 v2, DDR3, GPU 없음)에서 Google의 Gemma 4 26B MoE 모델을 초당 5토큰가량으로 돌렸다는 얘기다. 박스 값은 300달러도 안 든다.
왜 이게 우리한테 의미가 있냐. 온프레미스 굴리는 팀이라면 데이터센터나 사무실 랙에 은퇴 직전이거나 이미 은퇴한 엔터프라이즈 서버가 한두 대씩 굴러다닌다. 스토리지 노드 교체하고 남은 구형 박스, 리스 만료돼서 반납 안 하고 방치한 서버 같은 것들. 이걸 폐기하거나 창고에 처박아 두는 대신 "LLM 폴백 서버"나 "야간 배치용 추론 노드"로 재활용할 수 있다면 얘기가 달라진다.
여기서 짚고 넘어갈 점. 원문 저자도 강조하지만 이건 "GPU 대신 CPU 쓰면 됩니다" 같은 단순한 얘기가 아니다. MoE 아키텍처 모델이라 활성 파라미터가 적어서(26B-A4B, 즉 총 26B지만 토큰당 활성 4B) CPU에서도 견딜 만한 속도가 나온 거고, 저자의 CPU는 AVX2도 FMA3도 없는 세대라 기존 최적화 코드가 아예 빌드조차 안 됐다. 그걸 패치해서 겨우 돌린 사례다. 그냥 따라한다고 되는 게 아니라는 뜻이다.
2. 핵심: CPU 추론이 왜 되고, 왜 느린가
MoE 덕분에 26B가 CPU에서 견딘다
Gemma 4 26B-A4B는 Mixture-of-Experts 모델이다. 전체 파라미터는 26B지만 토큰 하나 생성할 때 실제로 계산에 참여하는 건 4B(A4B의 A가 active) 정도만 활성화된다. 이게 CPU 추론에서 결정적이다.
비유하자면 이렇다. Dense 모델(모든 파라미터가 매 토큰마다 다 도는 모델)이 26명 전원이 매번 회의에 들어오는 조직이라면, MoE는 안건마다 관련 전문가 8명(원문 기준 layer당 8 active experts)만 불러 모으는 구조다. 나머지는 메모리에 대기만 하고 연산은 안 한다. CPU는 GPU만큼 병렬 연산이 세지 않으니, 매 토큰마다 도는 실제 연산량이 줄어드는 MoE 구조가 CPU에겐 큰 선물이다.
진짜 병목은 연산이 아니라 메모리 대역폭
여기가 CPU 추론의 핵심이다. LLM 디코딩(토큰 하나씩 뽑는 과정)은 대부분 연산 성능(FLOPS)이 아니라 메모리 대역폭에 묶인다(memory-bandwidth-bound). 원문 저자도 fused 커널을 못 써서 matmul을 두 번 나눠 돌리게 됐지만 "이 CPU는 어차피 메모리 대역폭에 묶여 있어서 손해 본 게 없다"고 명시했다.
왜 그런가. 토큰 하나 생성하려면 활성화된 가중치(Q8_0면 파라미터당 약 1바이트)를 전부 메모리에서 읽어와야 한다. 4B 활성 파라미터면 대략 4GB를 매 토큰마다 RAM에서 CPU로 긁어와야 한다는 뜻이다. DDR3 메모리 대역폭이 실효 40~50GB/s 수준이라 치면, 산술적으로 초당 10토큰 언저리가 물리적 상한선이 된다. 저자가 5.2 tok/s를 얻은 게 이 대역폭 벽을 감안하면 납득이 간다.
실무에서 이걸 기억해야 하는 이유. CPU 추론 튜닝할 때 "코어 수를 늘리면 빨라지겠지"라고 생각하기 쉬운데, 대역폭에 묶인 워크로드는 코어를 아무리 늘려도 메모리 컨트롤러가 못 따라와서 어느 지점부터는 스레드를 늘려도 속도가 안 오르거나 오히려 캐시 경합으로 느려진다. 물리 코어 수보다 메모리 채널 수와 대역폭이 더 중요하다.
3. 실무 관점: 세팅, 트레이드오프, 흔한 함정
llama.cpp / ollama 기본 세팅
먼저 AVX2가 있는 평범한(비교적 최신) CPU라면 굳이 원문의 고생을 따라할 필요 없다. Ollama나 llama.cpp 최신 빌드를 그대로 쓰면 된다. 가장 흔한 진입 경로는 Ollama다.
# Ollama 설치 (Linux)
curl -fsSL https://ollama.com/install.sh | sh
# CPU 정보 먼저 확인 - AVX2 지원 여부가 갈림길
lscpu | grep -o 'avx[0-9a-z_]*' | sort -u
출력이 이렇게 나오면 최신 최적화 커널을 쓸 수 있는 세대다.
avx
avx2
avx512f
avx512_vnni
반대로 원문 저자의 박스처럼 이렇게만 나오면 얘기가 완전히 달라진다.
avx
avx2가 안 보이고 avx만 있으면 Ivy Bridge(2013) 혹은 그 이전 세대다. 이 경우 대부분의 최신 추론 엔진 빌드가 런타임에 죽거나 아예 컴파일이 안 된다. 원문의 핵심 고생이 정확히 이 지점이다.
llama.cpp를 직접 빌드해서 스레드를 조절하려면 이런 식이다. (모델 경로와 파일명은 각자 환경에 맞게)
# llama.cpp 빌드 (AVX2 있는 일반적인 서버 기준)
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build
cmake --build build --config Release -j$(nproc)
# 실행: 물리 코어 수에 맞춰 스레드 지정
./build/bin/llama-cli \
-m ./models/gemma-model-Q8_0.gguf \
-p "온프레미스 서버 재활용 아이디어 3가지" \
-t 20 \
-c 4096
여기서 -t 20이 스레드 수다. 흔한 함정 하나: -t를 논리 코어(하이퍼스레딩 포함) 전부에 맞추면 오히려 느려지는 경우가 많다. 물리 코어 수부터 맞춰보고, 거기서 하나씩 조절하며 tok/s를 실측하는 게 정석이다. 앞서 말했듯 대역폭 병목이라 코어 다 갈아넣는다고 안 빨라진다.
양자화(Quantization): Q4 vs Q8 트레이드오프
원문은 Q8_0으로 돌렸다. 여기서 실무자가 알아야 할 트레이드오프를 정리한다. (아래 원리는 일반적으로 통용되는 내용이고, 원문에 없는 구체 벤치마크 수치는 지어내지 않았다.)
- Q8_0: 파라미터당 약 8비트(1바이트). 품질 손실이 거의 없다시피 하지만 메모리를 많이 먹고, 매 토큰마다 읽어야 할 데이터가 크다 → 대역폭 병목 환경에선 속도가 상대적으로 느려진다.
- Q4_K_M 등 4비트 계열: 파라미터당 약 4비트. 메모리 사용량과 대역폭 부담이 절반 수준으로 줄어 같은 하드웨어에서 디코딩 속도가 더 나올 여지가 크다. 대신 품질(특히 정밀한 추론, 코드 생성)에서 미세한 손실이 생길 수 있다.
대역폭에 묶인 CPU 환경에서는 "읽어야 할 바이트 수 = 속도"에 직결되기 때문에, Q8에서 Q4로 내리면 속도 이득이 GPU 환경보다 더 체감된다. 다만 원문 저자가 Q8_0을 쓴 이유가 있을 수 있다 — AVX1 폴백 경로에서 특정 양자화 포맷만 검증됐거나, 4비트 K-quant 계열이 해당 하드웨어의 스칼라 폴백에서 제대로 도는지 별도 확인이 필요하다. Q4로 바꿔 돌릴 땐 반드시 출력 품질을 눈으로 검증하고 나서 프로덕션에 올려라.
흔한 함정: AVX2 없는 하드웨어의 조용한 오작동
원문에서 가장 무서운 대목이 이거다. 빌드 자체가 죽으면 차라리 낫다. 진짜 문제는 빌드도 되고 실행도 되는데 출력이 조용히 쓰레기가 나오는 경우다.
저자의 경우 GGML_USE_IQK_MULMAT를 끄고 빌드했는데, 그래프 빌더는 여전히 MOE_FUSED_UP_GATE 같은 fused 연산을 뱉는데 dispatcher의 switch문엔 해당 op에 대한 case가 없어서 default로 빠졌고, 그 결과 모든 expert FFN의 결과 텐서가 계산되지 않고 메모리 쓰레기값으로 남았다. 30 layer × 8 active experts = 매 forward pass마다 약 240개 텐서가 초기화 안 된 버퍼값이었다는 것.
증상이 특히 교활하다. NaN도 안 뜨고, 크래시도 안 나고, temperature 0에서 결정적(deterministic)으로 나오는데 결과물이 이런 식이다:
# 증상: 유창해 보이는 다국어 gibberish
# 태국어, 한국어, 안 쓰는 sentinel 토큰, 영어 조각이
# 262K vocabulary 전체에 균일하게 흩뿌려짐
Prompt: What is the capital of France?
Output: สวัสดี 국가 the の paris ที่ 답 ▁▁ करता...
이게 왜 무서우냐. 크래시가 나면 "뭔가 잘못됐네"하고 바로 알지만, 그럴싸한 텍스트가 나오면 세팅이 잘 된 줄 착각하기 쉽다. 저자는 sampling 직전 raw logits를 찍어서 첫 토큰의 mean logit이 0 근처여야 하는데 +16으로 튀어 있고 vocabulary의 약 80%가 양수 logit인 걸 보고 "residual stream의 큰 덩어리가 초기화 안 된 메모리(작은 양수 float들)"라고 진단했다.
실무 교훈: 비표준 하드웨어에서 추론 엔진을 돌릴 땐 반드시 "정답을 아는 프롬프트"로 sanity check를 먼저 해라. "2 + 2는?", "한국의 수도는?" 같은 걸로 정상적인 답이 나오는지부터 확인하고, 그 다음 실제 워크로드를 태워야 한다. 크래시 안 났다고 정상이라 믿으면 안 된다.
또 하나의 함정: --run-time-repack
원문이 명시적으로 경고하는 지점. --run-time-repack 플래그는 시작할 때 양자화 가중치를 Q8_0_R8이라는 AVX2 전용 인터리브 레이아웃으로 재배열한다. AVX1 CPU에서 이걸 켜면 위와 똑같은 gibberish가 다시 나온다. AVX2 없는 박스에서 돌릴 거면 이 플래그를 빼야 한다. (AVX2 있는 서버라면 오히려 켜서 이득 보는 옵션이니 무조건 빼라는 얘기는 아니다.)
대안
- 그냥 최신 CPU 서버를 쓴다: Haswell(2014, v3) 이상이면 AVX2/FMA3가 있어서 이 고생을 안 해도 된다. 사내에 놀고 있는 v3~v4 세대 Xeon이 있다면 그걸 먼저 노려라. 패치 없이 Ollama 바로 돌아간다.
- MoE 모델을 우선 고른다: Dense 26B를 CPU에서 돌리면 A4B MoE보다 훨씬 느리다. CPU 서빙 계획이면 애초에 MoE 계열 모델을 선택하는 게 전략적으로 맞다.
- 배치/비동기 워크로드로 한정: 5 tok/s는 사람이 읽는 속도지 대화형 실시간 서비스엔 부족하다. 야간 배치 요약, 로그 분류, 문서 태깅 같은 지연 시간이 관대한 작업에 붙이는 게 현실적이다.
4. 정리: 한 줄 요약과 적용 대상
한 줄 요약: MoE 모델과 메모리 대역폭 특성 덕분에 GPU 없는 구형 Xeon에서도 26B LLM을 읽는 속도(약 5 tok/s)로 돌릴 수 있지만, AVX2 없는 세대는 엔진 패치와 조용한 오작동 검증이라는 대가가 따른다.
이럴 때 써라:
- 사무실/데이터센터에 놀고 있는 구형 서버가 있고, 유료 API가 다운됐을 때의 로컬 폴백이나 비용 민감한 야간 배치 추론이 필요한 경우
- 토큰당 과금이 부담스럽고 지연 시간에 관대한 대량 텍스트 처리(분류, 요약, 태깅) 워크로드
- 데이터를 외부로 못 내보내는 규제 환경에서 저비용 온프레미스 추론이 필요한 경우
이럴 땐 쓰지 마라:
- 실시간 대화형 챗봇처럼 응답 속도가 UX인 서비스 (5 tok/s는 답답하다)
- 가진 서버가 Haswell 이전 세대인데 C++ 커널 디버깅까지 감당할 팀이 없는 경우 — 이땐 그냥 GPU 인스턴스 빌리거나 최신 CPU 서버 쓰는 게 인건비 대비 싸다
가장 중요한 건 원문 저자가 던진 메시지다. "AI를 잘한다"는 게 구독료 내는 걸 뜻하는 시대에, 진짜 레버리지는 남이 안 만들어준 문제에 모델을 겨눌 줄 알고, 나온 답이 진짜 맞는지 판별할 줄 아는 것이라는 점. 이번 사례에서도 실제 커널 패치는 Claude가 짰지만, 실험을 설계하고 "logits mean이 +16이면 이상하다"를 아는 건 사람의 몫이었다. 인프라 하는 우리한테 딱 필요한 감각이다.
참고 자료
'Tech_News' 카테고리의 다른 글
| Cloudflare Precursor: 체크포인트 봇 탐지를 버리고 전체 세션 행동을 스트림으로 보는 이유 (0) | 2026.07.16 |
|---|---|
| lobste.rs는 왜 MariaDB를 버리고 SQLite로 갔나 — DB를 줄여서 얻은 것들 (0) | 2026.07.15 |
| Claude Code가 프롬프트도 읽기 전에 33k 토큰을 태우는 이유 — OpenCode와 실측 비교 (0) | 2026.07.14 |
| PgBouncer를 4배 빠르게: so_reuseport 기반 멀티프로세스 풀러 아키텍처 뜯어보기 (1) | 2026.07.13 |
| ingress-nginx 은퇴, 이제 뭘 써야 하나: Gateway API·Contour·Traefik 실전 마이그레이션 (0) | 2026.07.11 |
