-
GPU 스케줄러를 Ollama warmup에서 vLLM 컨테이너로 옮긴 과정 — 시작·종료 시퀀스를 다시 짜다IT 2026. 5. 6. 23:30
들어가며 — "GPU 한 장에 여러 작업"이라는 제약
홈서버 DGX Spark에는 GPU가 한 장이다. 그런데 그 위에서 돌아야 하는 작업이 여럿이다.
- LLM 채팅 서비스 (vLLM Qwen3.6, 80GB 점유)
- VLM 사진 분석 배치 (gemma3-vision, 30GB 점유)
- 음성 전사 파이프라인 (whisper + pyannote, 15GB 점유)
- 가끔 도는 동영상 인코딩 (NVENC만 쓰지만 GPU 점유)
한 GPU에 다 동시에 못 올라간다. 그래서 자체 만든 단순한 GPU 스케줄러가 우선순위 기반 메모리 예산으로 시간차 분배를 한다. LLM이 한참 안 쓰이면 VLM이 들어와 사진 분석을 돌리고, LLM 요청이 들어오면 VLM을 일시 중지시키고 LLM이 GPU를 잡는 식이다.
Ollama → vLLM 마이그레이션을 하면서 이 스케줄러의 "LLM 시작·종료" 시퀀스를 다시 짜야 했다. Ollama의 가벼운 데몬 모델과 vLLM의 무거운 컨테이너 모델은 운영 방식이 완전히 다르기 때문이다. 이 글은 그 과정의 기록이다.
1. Ollama 시절의 시작·종료 시퀀스
Ollama의 좋은 점은 "지금 메모리에 모델이 있는지"를 직접 물어볼 수 있다는 것이다.
/api/ps엔드포인트가 그 정보를 준다. 그래서 idle 회수 로직이 단순했다 — "예산 잠겼는데 모델은 메모리에 없다 → idle이라 판단 → release."
2. vLLM은 그게 안 된다
vLLM에는
/api/ps같은 엔드포인트가 없다. 컨테이너가 살아 있으면 모델은 무조건 메모리에 떠 있다. 떠 있는지 묻는 건 의미가 없고, 있다/없다가 컨테이너의 존재 여부와 같다.그리고 콜드 스타트가 길다 — Ollama는 ~30초였는데 vLLM은 5~6분.
이전 Ollama 시절 health-poll 타임아웃은 120초로 충분했지만, vLLM에서는 첫 요청이 무조건 timeout으로 503을 받는다. HEALTH_POLL_TIMEOUT을 600초로 늘려서 콜드 스타트를 그냥 기다리게 했다. 그리고 idle 정책도 다시 짜야 했다.
3. 새 시작·종료 시퀀스
4. lock 파일 timestamp가 왜 좋은 답인가
이 패턴의 격언은 단순하다. "여러 경로가 같은 상태를 공유해야 한다면, 그 상태는 파일 시스템에 둬라." 분산 시스템 설계의 작은 격언인데, 단일 호스트에서도 cron + 프록시 + CLI 같은 여러 진입점이 있으면 그대로 적용된다. 메모리 변수에 두면 진입점 사이에서 보이지 않고, 같은 변수를 누가 어떻게 갱신하는지 추적이 안 된다.
5. 콜드 스타트 정책 — IDLE_TIMEOUT을 5분 → 30분으로
이전 idle은 5분이었다. cron이 30분마다 도는데 프록시가 먼저 5분으로 죽이는 셈이라, "방금 채팅 끝내고 5분 다른 일 하다 돌아오면 다시 6분 콜드 스타트" 패턴이 반복됐다.
30분이라는 숫자는 cron 회수 주기와 맞춘 것이다. 프록시가 먼저 죽이지 않고 cron에 idle 판단을 맡긴다. 메모리 예산을 다른 작업이 80GB만큼 필요로 하면 우선순위 시스템이 어차피 vllm-chat을 선점하니, 30분 점유 비용도 크지 않다.
정리
"백엔드만 바꿨다"가 아니라, GPU 자원 운영 시퀀스 전체를 다시 짜야 했다.
- Ollama → vLLM: 가벼운 데몬 + warmup(30초) → Docker 컨테이너 + 긴 콜드 스타트(5~6분).
- health 폴링: timeout 120초 → 600초로 늘려 콜드 스타트 수용.
- idle 회수 모델:
/api/ps같은 엔드포인트가 사라졌으므로, lock 파일 timestamp를 단일 진실 소스로 삼음. - idle 정책: 5분 → 30분으로 늘려, 짧은 휴식 후 매번 6분 콜드 스타트하는 사고 제거.
한 줄로 줄이면, "가벼운 데몬용 운영 패턴은 무거운 컨테이너에 그대로 못 쓴다"이다. 벤치마크 숫자만 보면 백엔드 교체는 단순한 "옵션 A → 옵션 B" 같지만, 운영 시퀀스 전체가 다시 설계돼야 한다. 이 부분이 마이그레이션 작업의 절반 이상을 잡아먹는다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
로컬 챗봇 시리즈 #4 — Vision 32B에서 7B로, 그리고 포기까지 — 두 vLLM 동거 시행착오 (0) 2026.05.08 로컬 챗봇 시리즈 #3 — 도구에 '지금 누구의 첨부인지' 어떻게 알려주나: ContextVar 패턴 (0) 2026.05.08 로컬 챗봇 시리즈 #2 — Project 시스템 프롬프트는 왜 글로벌 Custom Instructions '다음에' 와야 하나 (0) 2026.05.08 로컬 챗봇 시리즈 #1 — 메시지 편집은 왜 그렇게 단순해야 하나: 컨텍스트 엔지니어링 관점에서 (0) 2026.05.08 Ralph Loop — bash while true + LLM CLI가 만든 어이없게 강력한 에이전트 패턴 (0) 2026.05.07 RAG 청크 맥락에서 thinking을 꺼야 하는 이유 — enable_thinking=False가 필요한 순간 (0) 2026.05.06 OpenAI-compat 표준화로 어댑터 100줄 들어내기 — passthrough가 가져온 코드 청결도 (0) 2026.05.06 vLLM reasoning_parser — <think> 블록을 정규식 말고 구조로 받는 법 (0) 2026.05.06 KV cache FP8로 동시 요청 76배 수용하기 — LLM 메모리의 숨은 주범 정리 (0) 2026.05.06 Ollama에서 vLLM으로 백엔드를 바꿨더니 throughput이 148% 올랐다 — 같은 모델, 다른 엔진 (0) 2026.05.06