지난 글의 계산에는 조용한 전제가 하나 있었다. 복제본 하나가 카드 한 장을 통째로 쓴다는 전제다. 3B 모델을 A100 80GB에 올려 두고 초당 두어 건을 처리하는 상황이라면 그 전제부터 아깝다. 메모리도 계산 자원도 대부분 놀고 있는데 카드 값은 다 낸다.
그럼 두 프로세스를 같은 카드에 그냥 띄우면 어떻게 되는가. 된다. 다만 무엇이 어떻게 되는지 모르고 하면 원인 모를 지연 요동과 메모리 부족을 만난다.
아무 설정도 안 하면 시분할이다
기본 동작부터 알아 두는 편이 낫다. 한 GPU에 여러 프로세스가 커널을 올리면 드라이버가 번갈아 실행한다.
①이 그 모습이다. 어느 순간에도 한 프로세스만 돌고, 전환할 때마다 문맥을 갈아 끼운다. 여기서 두 가지가 나온다.
- 처리량은 거의 안 는다. 어차피 한 번에 하나만 도니 나눠 쓴다고 계산이 더 되지 않는다
- 지연은 나빠지고, 무엇보다 예측이 안 된다. 내 요청이 언제 차례를 받을지가 옆 프로세스가 얼마나 오래 커널을 붙잡는지에 달렸다
메모리는 그나마 나눠 갖는다. 다만 칸막이가 없다. 옆 프로세스가 배치를 키워 메모리를 다 먹으면 내 쪽이 메모리 부족으로 죽는다. 서빙 엔진은 대개 시작할 때 남은 메모리의 90%를 KV 캐시로 미리 잡아 두므로(vLLM의 gpu_memory_utilization), 두 번째 프로세스는 자리가 없어 아예 못 뜨는 경우가 더 흔하다.
MPS — 커널을 같이 올린다
MPS(Multi-Process Service)는 여러 프로세스의 커널을 하나의 스케줄러 아래로 모아 동시에 올린다. 그림 ②처럼 A와 B가 같은 시간대에 함께 돈다.
이득은 빈자리를 메우는 데서 온다. 작은 모델의 커널은 GPU의 연산 유닛을 다 채우지 못하는데, 그 빈 유닛에 다른 프로세스의 커널이 들어간다. 배치가 작고 커널이 자잘한 워크로드에서 특히 효과가 크다.
대가는 격리다.
- 메모리 칸막이가 없다. 상한을 걸 수는 있지만(
CUDA_MPS_PINNED_DEVICE_MEM_LIMIT) 하드웨어가 강제하는 것이 아니다 - 오류 격벽이 없다. 한 프로세스가 메모리 오류로 죽으면 같은 MPS 서버에 붙은 다른 프로세스까지 함께 넘어질 수 있다
- 성능이 이웃에 좌우된다. 옆이 조용하면 빠르고 붐비면 느리다. 지연 시간 약속을 걸기 어렵다
연산 자원의 비율은 정할 수 있다(CUDA_MPS_ACTIVE_THREAD_PERCENTAGE). 다만 이것도 상한일 뿐 최소 보장이 아니라서, 「최소한 이만큼은 준다」는 약속에는 못 쓴다.
MIG — 카드를 하드웨어로 자른다
MIG(Multi-Instance GPU)는 접근이 다르다. 카드를 여러 개의 작은 GPU로 물리적으로 쪼갠다. 연산 유닛, 메모리, L2 캐시, 메모리 대역폭까지 갈라서 각 조각에 붙인다.
그림 ③에서 두 칸이 서로 남남처럼 보이는 이유가 그것이다. 조각 하나는 운영체제에 별개의 GPU로 보이고, 옆칸이 무엇을 하든 내 성능은 그대로다. 지연 시간 약속을 걸 수 있는 유일한 방식이다.
A100 80GB의 프로파일을 예로 들면 이렇게 나뉜다.
| 프로파일 | 메모리 | 한 장에서 몇 개 | 어울리는 것 |
|---|---|---|---|
1g.10gb |
10GB | 7 | 임베딩·재순위·분류 모델 |
2g.20gb |
20GB | 3 | 3B 안팎 모델 서빙 |
3g.40gb |
40GB | 2 | 8B 모델 서빙 |
7g.80gb |
80GB | 1 | 나누지 않음 |
제약도 분명하다.
- A100·H100 같은 데이터센터 카드에서만 된다. L40S나 소비자용 카드에는 없다
- 나눈 뒤에는 못 빌려준다. 내 칸이 놀아도 옆칸이 못 쓴다. 이용률로 보면 오히려 손해가 날 수 있다
- 바꾸려면 비운다. 프로파일을 다시 짜려면 그 카드의 작업을 전부 내려야 한다
- 조각끼리 묶어 못 쓴다. 텐서 병렬처럼 여러 조각을 합쳐 큰 모델을 올리는 것은 안 된다
셋을 나란히 놓으면
| 시분할 | MPS | MIG | |
|---|---|---|---|
| 동시에 도는가 | 아니오 | 예 | 예 |
| 메모리 칸막이 | 없음 | 소프트웨어 상한 | 하드웨어 |
| 오류 격벽 | 있음 | 없음 | 있음 |
| 성능 예측 | 나쁨 | 중간 | 좋음 |
| 자원 되빌려주기 | 됨 | 됨 | 안 됨 |
| 필요한 하드웨어 | 아무거나 | 아무거나 | A100·H100 계열 |
| 켜는 법 | 기본값 | 데몬 실행 | 카드를 내리고 재구성 |
「무엇이 제일 좋은가」로는 답이 안 나온다. 격리와 이용률을 맞바꾸는 축 위에 셋이 놓여 있어서, 어느 쪽이 더 급한지가 정한다.
쿠버네티스에서 주의할 것
쿠버네티스에 GPU를 붙일 때 흔히 쓰는 설정이 하나 있다. NVIDIA 디바이스 플러그인의 time-slicing 옵션인데, 카드 한 장을 replicas: 4로 선언하면 파드 넷이 각각 GPU 하나를 받았다고 여기게 만든다.
이름 그대로 시분할이라는 점을 오해하지 않는 것이 중요하다. 자원을 나눈 것이 아니라 스케줄러를 속인 것이다. 넷이 각자 「GPU 한 장」을 받았다고 믿고 저마다 메모리의 90%를 잡으려 들면 세 개가 못 뜬다. 서빙 엔진을 이 방식으로 여럿 올릴 때는 각 파드의 메모리 비율을 손으로 나눠 줘야 한다(넷이면 gpu_memory_utilization을 0.22쯤으로).
MIG는 반대로 정직하다. 조각마다 별개의 장치로 보이므로 플러그인이 nvidia.com/mig-3g.40gb 같은 자원 이름으로 내보내고, 파드는 진짜로 그 칸만 받는다.
나누기 전에 물어볼 것
카드를 나누는 방법을 고르기 전에, 나눌 필요가 있는지를 먼저 보는 편이 낫다. 이용률이 낮은 이유가 대개 둘 중 하나이기 때문이다.
- 동시 요청이 적어서라면 나누는 것이 맞다
- 배치가 안 차서라면 연속 배칭이 제대로 도는지부터 본다. 요청은 충분한데 하나씩 처리하고 있었다면, 카드를 나누는 것보다 엔진 설정 한 줄이 훨씬 크게 바꾼다
모델이 여러 개인 것이 이유라면 선택지가 하나 더 있다. 같은 베이스의 파생 모델이라면 카드를 나눌 것 없이 어댑터를 함께 태우는 방식으로 프로세스 하나에 담을 수 있다. 나누지 않고 합치는 쪽이 이용률에는 언제나 유리하다.
읽어주셔서 감사합니다. 😊

