[LLM 추론] CPU 스레드와 NUMA 메모리 접근
GPU 자원이 부족한 환경에서 CPU·RAM·디스크의 역할을 구분하고, 스레드 수와 NUMA 메모리 배치가 추론 처리량에 미치는 영향을 설명합니다.
개요
큰 모델을 실행하려는데 GPU 자원, 특히 VRAM이 부족하면 서버에 남아 있는 CPU·RAM·디스크의 활용을 고려합니다.
목적은 GPU에 담기지 않는 모델을 실행하면서 가능한 처리량을 확보하는 것입니다.
다만 자원을 더 많이 쓰는 것과 더 빠르게 처리하는 것은 다릅니다.
- GPU·VRAM: GPU가 계산할 가중치와 실행 상태를 보관하고 연산을 수행합니다.
- RAM: VRAM에 담지 못한 가중치 등을 보관합니다. 용량뿐 아니라 데이터를 읽는 대역폭도 중요합니다.
- CPU: 실행 엔진이 CPU에 배정한 연산과 데이터 준비를 수행합니다. 스레드 수를 늘린다고 GPU 연산이 자동으로 CPU에 분담되지는 않습니다.
- 디스크: 모델 파일을 저장하고 적재할 때 데이터를 공급합니다. 실행 중 필요한 데이터를 반복해서 디스크에서 읽어야 한다면 I/O 대기가 병목이 될 수 있습니다.
RAM이 충분해 필요한 데이터가 메모리에 상주한다면 디스크는 주로 초기 적재에 관여합니다.
이때 디스크를 더 적극적으로 사용한다고 생성 TPS가 높아지는 것은 아닙니다.
이 문서는 이러한 혼합 실행에서 CPU를 늘렸을 때 효과가 달라지는 이유를 설명합니다. 현재 서버의 최적값을 확정한 실험 결과는 아닙니다.
로컬·원격 메모리 접근 차이
NUMA에서는 CPU가 접근하는 메모리 위치에 따라 지연과 대역폭 특성이 달라집니다.
일반적인 공유 메모리 NUMA 서버에서는 다른 노드의 RAM도 접근할 수 있습니다.
특정 CPU만 특정 RAM을 읽을 수 있다는 의미가 아닙니다.
CPU A 쪽 RAM에 데이터가 있는 상황을 단순화하면 다음과 같습니다.
CPU A ── 로컬 메모리 컨트롤러 ── RAM A
CPU B ── 노드 사이 연결 ── A 쪽 메모리 컨트롤러 ── RAM A
- 로컬 접근: 계산하는 CPU와 가까운 노드의 RAM을 읽습니다.
- 원격 접근: 다른 노드의 RAM을 읽으므로 추가 연결 경로를 거칩니다.
- NUMA 노드: 메모리 접근 특성을 구분하는 단위입니다. CPU 소켓 하나가 여러 NUMA 노드로 나뉠 수도 있습니다.
CPU B가 데이터를 읽지 못해서 느린 것이 아닙니다.
읽을 수는 있지만 더 먼 경로와 공유 자원을 사용하기 때문에 비용이 추가될 수 있습니다.
이 구조는 Linux 커널 NUMA 문서에서 설명합니다.
코어 확장의 한계
데이터를 RAM A에 둔 채 계산할 CPU만 늘리는 상황을 생각해볼 수 있습니다.
추가 코어가 효과를 내려면 데이터를 공급하는 경로에도 여유가 있어야 합니다.
- CPU A의 코어들이 RAM A에서 데이터를 읽고 계산합니다.
- RAM A의 메모리 대역폭이 포화되면 코어를 추가해도 데이터 공급량은 충분히 늘지 않습니다.
- CPU B까지 참여하면 원격 접근이 추가됩니다. 노드 사이 연결과 스레드 동기화가 새로운 비용이 될 수 있습니다.
반대로 계산량이 병목이라면 코어 추가가 도움이 됩니다.
데이터와 작업을 여러 노드에 적절히 분산하면 여러 메모리 채널을 활용할 여지도 있습니다.
따라서 전체 CPU 사용과 한 노드 고정 중 어느 쪽이 항상 빠르다고 정할 수는 없습니다.
CPU 고정과 메모리 배치의 구분
CPU 실행 위치를 제한해도 데이터가 그 CPU 가까이에 자동으로 재배치되는 것은 아닙니다.
실행 스레드 수, 실행 위치, 메모리 할당 정책은 각각 다른 설정입니다.
| 설정 | 제어하는 대상 | 주의할 점 |
|---|---|---|
| CPU threads | 계산에 참여할 스레드 수 | 허용된 CPU 범위를 넓히지는 않습니다. |
taskset | 실행 가능한 논리 CPU 집합 | 메모리 배치를 지정하지 않습니다. |
| NUMA 메모리 정책 | 새 메모리를 할당할 노드와 방식 | 기존 페이지가 모두 이동하는 것은 아닙니다. |
예를 들어 32개 논리 CPU만 허용한 상태에서 계산 스레드를 128개로 늘려도 128개 CPU를 사용하는 것은 아닙니다.
같은 CPU를 더 많은 스레드가 나눠 쓰면서 비용이 커질 수 있습니다.
메모리를 특정 노드에 강하게 제한하려면 해당 노드의 여유 용량도 확인해야 합니다.
서버 전체 RAM이 충분해도 허용된 노드의 메모리가 부족하면 할당에 실패할 수 있습니다.
정책의 적용 범위와 할당 동작은 Linux NUMA 메모리 정책 문서를 참고합니다.
CPU·GPU 혼합 실행의 병목
GPU와 시스템 RAM을 함께 사용하는 실행에서는 CPU 계산 외에도 가중치 읽기와 GPU 전송이 시간을 차지할 수 있습니다.
구현에 따라 RAM의 데이터를 CPU가 계산하거나 GPU로 전달합니다. GPU 캐시에 이미 있는 가중치는 다른 경로를 사용할 수 있습니다.
- CPU 계산 병목: 코어를 늘려 계산 시간을 줄일 가능성이 있습니다.
- RAM 접근 병목: 데이터 배치와 메모리 대역폭이 중요합니다.
- GPU 전송 병목: CPU 스레드만 늘려서는 전송 경로의 한계를 해결하지 못합니다.
- GPU 계산 병목: CPU 사용률이 낮아도 전체 처리량을 제한하는 자원은 GPU일 수 있습니다.
이는 병목의 후보이며 현재 서버에서 측정으로 확정한 원인은 아닙니다.
CPU 사용률이 낮다는 관측 하나만으로 코어가 부족한지, 메모리를 기다리는지, GPU를 기다리는지 구분할 수 없습니다.
성능 차이와 적용 범위
CPU 스레드를 무조건 늘려도 빨라지지 않는다는 주장은 llama.cpp 공식 성능 안내에서도 확인할 수 있습니다.
안내에서는 과도한 스레드 수를 점검하고 적은 수에서 늘려가며 성능이 떨어지는 지점을 찾도록 설명합니다.
다만 이 설명이 현재 서버에서 CPU가 병목이라는 증거는 아닙니다.
공식 문서의 예시는 A6000 48GB, 물리 CPU 7코어, RAM 32GB, 30B Q4 모델 조건입니다.
같은 GPU 오프로딩 옵션에서 스레드 수를 바꾼 결과는 다음과 같습니다.
| CPU 스레드 | 생성 속도 |
|---|---|
| 1 | 5.5 tokens/s |
| 4 | 9.1 tokens/s |
| 7 | 8.7 tokens/s |
이 사례에서는 1→4스레드가 약 65% 빨랐지만 4→7스레드는 약 4% 느렸습니다.
코어 추가의 효과가 일정하지 않다는 근거이며, NUMA 효과를 분리한 실험은 아닙니다.
오래된 별도 모델·하드웨어의 결과이므로 현재 DeepSeek 구성의 예상 개선율로 사용할 수 없습니다.
- llama.cpp 생성 성능 점검 안내.
현재 구성의 차이는 CPU·메모리 경로가 전체 시간에서 차지하는 비중에 달려 있습니다.
예를 들어 서로 겹치지 않는 처리 시간 중 20%인 구간만 두 배로 빨라진다면 전체 속도는 약 1.11배입니다.
같은 조건에서 개선 구간이 80%라면 약 1.67배가 됩니다. 이는 영향 범위를 설명하는 계산 예시이며 실측이나 예측값이 아닙니다.
따라서 CPU·RAM 접근이 주된 병목이면 차이가 클 수 있습니다.
GPU 계산이나 전송이 시간을 대부분 차지한다면 CPU 스레드 조정만으로 얻는 효과는 작을 수 있습니다.
NUMA 접근 특성도 플랫폼마다 다르므로 일률적인 배수를 붙이지 않습니다. Linux NUMA 메모리 성능 문서는 지연과 대역폭을 별도 특성으로 다룹니다.
성능 비교 기준
NUMA 최적화의 목적은 CPU 사용률을 높이는 것이 아니라 요청 처리 시간을 줄이는 것입니다.
같은 조건에서 실행 위치와 스레드 수를 바꾸고 실제 처리량을 비교해야 합니다.
- 모델·양자화·GPU 캐시·배치·Thinking 설정을 유지합니다.
- 입력과 출력 길이를 맞추고 첫 요청과 캐시 재사용 요청을 구분합니다.
- 입력 처리 속도와 생성 TPS를 따로 기록합니다.
- CPU 범위를 넓힐 때 스레드 수와 메모리 배치를 함께 기록합니다.
- 한 번의 결과보다 여러 번 실행했을 때의 편차와 일관성을 확인합니다.
CPU 사용률만 높아지고 TPS가 그대로라면 해당 조건에서는 코어를 추가해도 실익이 작습니다.
NUMA 최적화는 계산하는 CPU와 필요한 데이터의 위치를 함께 맞추는 작업입니다.