모델 운영

MLOPS / 86번째 글

CPU만으로 LLM을 돌린다는 것

GPU 없이 모델을 서빙해야 할 때 무엇이 천장을 정하는지 정리합니다. 대역폭이 초당 토큰 수를 어떻게 묶는지, 양자화가 왜 CPU에서 더 효과적인지, 스레드와 배치를 어떻게 잡을지 다룹니다.

PALDYN Team14 MIN READ

GPU 없이 언어 모델을 돌려야 하는 자리가 생각보다 많다. 사내망 안에서만 도는 도구라 GPU 인스턴스를 새로 사기 어렵거나, 요청이 하루에 몇십 건뿐이라 GPU를 놀리는 값이 더 크거나, 문서가 밖으로 나가면 안 되는데 온프레미스 장비에 GPU가 없거나 하는 경우다. 지난 글이 GPU의 빈틈을 쓰는 이야기였다면 이번에는 GPU가 아예 없는 자리다.

결론부터 말하면 된다. 다만 무엇을 기대하고 무엇을 포기해야 하는지가 GPU와 완전히 다르고, 그 선을 모른 채 시작하면 "느려서 못 쓰겠다"로 끝난다.

천장은 계산이 아니라 대역폭이 정한다

CPU 추론의 성능을 예측하는 데는 스펙 시트의 한 줄이면 충분하다. 메모리 대역폭이다.

토큰을 하나 뽑을 때 모델의 가중치 전부를 메모리에서 읽어야 한다. 8B 모델을 4비트로 담으면 약 4.5GB이고, 이 4.5GB를 초당 몇 번 읽을 수 있는지가 곧 초당 토큰 수의 상한이다.

CPU와 GPU의 메모리 대역폭 비교

DDR5 듀얼 채널 데스크톱이 약 100GB/s이니 100÷4.5≈22100 \div 4.5 \approx 22, 초당 22토큰이 이론상 한계다. 실측은 그 절반에서 7할쯤 나와 1216토큰 정도다. 사람이 읽는 속도가 초당 58토큰이니 한 명이 읽기에는 충분하고, 동시에 다섯 명이 붙으면 못 버틴다.

이 계산이 유용한 이유는 하드웨어를 사기 전에 답이 나온다는 데 있다. 필요한 초당 토큰 수에 모델 파일 크기를 곱하면 필요한 대역폭이 나오고, 그 값이 지금 장비의 대역폭을 넘으면 코드로는 해결이 안 된다. CPU 코어를 두 배로 늘려도 이 선은 안 움직인다 — 코어가 늘어도 메모리에서 데이터가 오는 속도는 그대로다.

그래서 CPU 추론에서 장비를 고를 때 봐야 할 것은 코어 수가 아니라 메모리 채널 수다. 8채널 서버 CPU는 코어가 같아도 듀얼 채널 데스크톱보다 두 배 빠르다. 애플 실리콘이 CPU 추론에서 유독 잘 나오는 것도 같은 이유다 — 통합 메모리의 대역폭이 200~800GB/s로 일반 PC보다 한 자릿수 위다.

양자화는 여기서 성격이 다르다

GPU에서 양자화는 주로 메모리를 아끼려고 한다. 모델이 VRAM에 들어가느냐 마느냐의 문제다.

CPU에서는 다르다. 양자화가 그대로 속도가 된다. 병목이 대역폭이므로 읽어야 할 바이트가 절반이 되면 속도가 대략 두 배가 된다. FP16으로 16GB인 8B 모델이 4비트로 4.5GB가 되면, 같은 장비에서 초당 토큰 수가 3배 넘게 오른다.

정밀도 8B 모델 크기 DDR5 듀얼 채널 실측 대역 대략적인 초당 토큰
FP16 약 16GB 100GB/s 3~4
8비트 약 8.5GB 100GB/s 6~8
4비트 약 4.5GB 100GB/s 12~16
3비트 약 3.6GB 100GB/s 15~19

품질 쪽도 같이 봐야 한다. 4비트는 대부분의 작업에서 손실이 눈에 안 띄는 지점이고, 3비트로 내려가면 긴 추론이나 코드 생성에서 무너지기 시작한다. 그래서 4비트가 사실상 기본값이고, 3비트는 메모리가 정말 모자랄 때만 쓴다.

한 가지 더. 양자화 형식마다 CPU에서의 연산 속도가 다르다. 가중치를 매번 부동소수점으로 되돌려 곱하는 형식은 대역폭은 아꼈어도 그 되돌리는 계산이 새 병목이 된다. 정수 명령어로 바로 곱할 수 있게 짜인 형식(llama.cpp의 Q4_K_M 같은 K-quant 계열)이 CPU에서는 눈에 띄게 낫다. GPU에서 좋았던 양자화가 CPU에서도 좋을 것이라고 가정하면 안 된다.

런타임은 세 갈래로 갈린다

CPU 추론에 쓰는 것은 대체로 셋 중 하나다.

런타임 강점 약한 자리
llama.cpp / GGUF 양자화 형식이 가장 잘 다듬어져 있고, 아무 장비에나 올라간다 지원 모델 구조가 나온 뒤에야 쓸 수 있다
ONNX Runtime 파이썬 밖으로 나가기 쉽고 다른 모델과 파이프라인을 같이 태운다 LLM 전용 최적화는 llama.cpp보다 덜 촘촘하다
OpenVINO 인텔 CPU에서 벡터 명령어를 가장 잘 쓴다 인텔 장비 밖에서는 이점이 사라진다

세 개를 다 재 볼 필요는 없다. 장비가 인텔이면 OpenVINO를, 아니면 llama.cpp를 기본으로 두고 시작해서 부족하면 그때 옮기는 정도가 현실적이다. 셋 다 같은 대역폭 천장 아래 있으므로 차이는 그 천장의 몇 할까지 가느냐이지 천장을 넘는 것이 아니다.

파이썬에서 llama.cpp를 붙이는 최소 형태는 이 정도다.

from llama_cpp import Llama

llm = Llama(
    model_path="models/qwen3-8b-q4_k_m.gguf",
    n_ctx=4096,
    n_threads=8,          # 물리 코어 수. 하이퍼스레드까지 세지 않는다
    n_batch=512,          # 프리필을 한 번에 처리하는 토큰 수
)

out = llm.create_chat_completion(
    messages=[{"role": "user", "content": "이 문단을 세 줄로 줄여 줘"}],
    max_tokens=256,
)
print(out["choices"][0]["message"]["content"])

프리필과 디코드가 서로 다르게 반응한다

CPU에서 튜닝할 때 가장 자주 헷갈리는 지점이다. 요청 하나는 프롬프트를 읽는 프리필과 토큰을 뽑는 디코드 두 단계로 나뉘는데, 이 둘이 병목이 다르다.

스레드 수에 따른 프리필과 디코드의 반응 차이

프리필은 프롬프트 전체를 한꺼번에 처리하므로 큰 행렬 곱이 되고, 그래서 계산이 병목이다. 코어를 늘리면 거의 비례해서 빨라진다. 디코드는 한 번에 토큰 하나라 계산량이 적고 대역폭이 병목이라, 코어를 늘려도 4개쯤에서 평평해진다.

여기서 나오는 실무 규칙이 둘이다.

스레드 수는 물리 코어 수에 맞춘다. 하이퍼스레드까지 세어 16을 잡으면 두 논리 코어가 같은 물리 코어의 메모리 포트를 다투게 되어 오히려 느려진다. 8코어 16스레드 CPU면 n_threads=8이다.

프롬프트가 길면 체감이 프리필에서 갈린다. RAG처럼 컨텍스트가 4,000토큰씩 들어가는 구조라면 첫 토큰까지의 시간이 전부 프리필이고, 이 구간은 코어를 늘리면 실제로 짧아진다. 반대로 짧은 질문에 긴 답을 뽑는 구조라면 코어를 늘려도 거의 안 변한다. 어느 쪽이 자기 트래픽인지를 먼저 보고 튜닝 대상을 정한다.

동시 요청은 다르게 설계해야 한다

GPU 서빙에서는 동시 요청이 늘어도 배치로 묶어 처리량을 지킨다. 가중치를 한 번 읽어 여러 시퀀스를 같이 계산하니 요청당 비용이 내려간다.

CPU에서도 원리는 같지만 효과가 훨씬 작다. 계산 여유가 애초에 없어서 배치를 키워도 금방 계산 쪽이 막힌다. 실제로는 동시 요청 4~8개쯤에서 요청당 속도가 사람이 못 기다릴 만큼 내려간다.

그래서 CPU 서빙은 큐를 앞에 두고 동시 실행 수를 명시적으로 제한하는 형태로 짜는 것이 낫다. 요청 20개가 한꺼번에 들어와 전부 초당 1토큰으로 기어가는 것보다, 4개씩 정상 속도로 처리하고 나머지는 대기시키는 쪽이 사용자 경험이 낫다. 대기 시간을 응답에 알려 주면 더 낫다.

그리고 애초에 CPU 추론이 맞는 워크로드인지를 먼저 본다. 하루 수백 건의 문서 요약, 사내 검색의 질의 재작성, 배치로 도는 분류 작업처럼 지연에 여유가 있고 동시성이 낮은 일에는 잘 맞는다. 실시간 대화형 서비스에 사용자가 수십 명 붙는다면 GPU 한 장이 CPU 서버 여러 대보다 싸다.

비용을 어떻게 볼 것인가

CPU 추론을 고르는 이유는 대개 비용이다. 그런데 비용 비교를 처리량 기준으로만 하면 CPU가 거의 항상 진다 — 토큰당 단가는 GPU가 압도적으로 싸다.

CPU가 이기는 자리는 가동률이 낮을 때다. GPU 인스턴스는 켜 두는 동안 계속 돈이 나가는데, 하루에 요청이 200건이면 GPU는 99% 놀면서 요금을 만든다. 이미 돌고 있는 애플리케이션 서버에 CPU 추론을 얹으면 추가 비용이 사실상 0이다.

정리하면 판단 기준은 이렇게 된다.

  • 요청이 드문드문하고 지연에 여유가 있다 → CPU. 놀리는 GPU 값을 안 낸다
  • 요청이 꾸준히 있고 동시성이 높다 → GPU. 토큰당 단가가 한 자릿수 차이로 싸다
  • 데이터가 밖으로 못 나가는데 GPU가 없다 → CPU. 이때는 비교가 아니라 유일한 선택지다
  • 지금 필요한 초당 토큰 수 × 모델 파일 크기 > 장비 대역폭 → 코드로 해결 안 된다. 모델을 줄이거나 장비를 바꾼다

정리

  • CPU 추론의 상한은 메모리 대역폭 ÷ 모델 파일 크기다. 이 값을 먼저 계산하면 장비를 사기 전에 가능 여부가 나온다
  • 코어 수가 아니라 메모리 채널 수를 본다. 코어를 늘려도 디코드는 안 빨라진다
  • 양자화가 그대로 속도다. 4비트가 기본값이고, CPU에서는 정수 곱으로 바로 처리되는 형식을 고른다
  • 스레드는 물리 코어 수까지만 잡는다. 프리필은 코어에 반응하고 디코드는 반응하지 않는다
  • 동시 실행 수를 큐로 제한한다. CPU는 배치로 처리량을 지켜 내지 못한다
  • 가동률이 낮은 워크로드에서만 GPU보다 싸다. 꾸준한 트래픽이면 GPU 쪽이 토큰당 훨씬 저렴하다

읽어주셔서 감사합니다. 😊

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

KV 캐시 양자화 — 가중치보다 이쪽이 먼저 넘친다

긴 문맥에서 GPU 메모리를 실제로 잡아먹는 것은 가중치가 아니라 KV 캐시입니다. 캐시 크기를 계산하는 법, K와 V를 다르게 다뤄야 하는 이유, 어디까지 줄여도 되는지를 정리합니다.

16 MIN
모델 운영2026.09.04

양자화 보정 — 데이터 128개가 모델 품질을 정한다

양자화에서 스케일을 정하는 절차가 보정입니다. 무엇을 재는지, 데이터를 어디서 몇 개 뽑아야 하는지, 자르는 지점을 어떻게 고르는지, 그리고 잘못된 보정이 어떤 모양으로 드러나는지를 정리합니다.

21 MIN
모델 운영2026.09.03

과제 특화 증류 — 큰 모델의 답을 작은 모델에 옮긴다

범용 성능이 아니라 우리 과제 하나만 잘하는 작은 모델을 만드는 방법입니다. 교사에게 무엇을 받아야 하는지, 데이터를 어떻게 모으고 거르는지, 손익 분기가 어디인지를 정리합니다.

15 MIN