GPU는 100%인데 학습이 멈췄습니다 — 16GB VRAM 로컬 AI 삽질기
안녕하세요 남도공입니다. 요즘 이미지 생성 AI를 클라우드가 아니라 집 컴퓨터에서 돌리고 있어요. ComfyUI라는 도구가 강력하긴 한데, 그림 한 장 뽑으려면 노드를 주렁주렁 연결해야 해서 — “프롬프트만 쓰면 나머지는 알아서 되는” 개인용 앱을 AI 코딩 도구랑 같이 만들었습니다.
목표는 세 개였어요. ①채팅하듯 말로 이미지 생성 ②만든 이미지를 다시 말로 수정 ③제 캐릭터를 학습(LoRA)시켜서 계속 쓰기.
앱 자체는 생각보다 금방 만들어졌는데, 진짜 고생은 전부 VRAM(그래픽카드 메모리) 16GB라는 한계에서 나왔습니다. 오늘 글은 그 삽질 기록이에요. 숫자는 전부 제 PC에서 실측한 값입니다. 환경 따라 다를 수 있어요.
시작 전에 — 잘한 선택 하나
앱을 별도 서버로 만들지 않고 ComfyUI 커스텀 노드로 만들었어요.
ComfyUI/custom_nodes/남도바나나/
├─ __init__.py # 라우트를 ComfyUI 서버에 얹음
└─ web/index.html # UI 전체 (단일 파일)
ComfyUI는 내부에 웹서버(aiohttp)를 이미 갖고 있어서, 커스텀 노드에서 라우트만 추가하면 같은 서버, 같은 포트에 제 API가 붙습니다.
from server import PromptServer
routes = PromptServer.instance.routes
@routes.get("/myapp/health")
async def health(request):
return web.json_response({"ok": True})
덕분에 따로 띄울 프로세스가 없고, 주소가 같으니 골치 아픈 설정(CORS)도 필요 없고, ComfyUI가 원래 제공하는 기능들을 그대로 가져다 씁니다. 로컬 도구를 만들 때 “서버를 하나 더 띄운다”는 생각보다 큰 비용이더라고요. 이미 돌고 있는 서버에 얹을 수 있는지부터 확인하는 걸 추천해요.
삽질 ① — 학습이 15배 느려진 이유는 설정 한 줄
캐릭터 LoRA 학습을 돌렸더니 한 스텝에 30~42초가 나왔습니다. 1500스텝이면 15시간이에요. 정상이면 2~3초여야 하는데요.
범인은 학습 설정의 이 한 줄이었습니다.
datasets:
- resolution: [ 512, 768, 1024 ] # ← 범인
이미지를 여러 해상도로 학습하는 옵션인데, 1024가 들어가는 순간 VRAM 요구량이 약 14GB로 뜁니다. 16GB 카드에서 바탕화면이랑 브라우저가 쓰는 2GB를 빼면 남는 게 없어요. 모자란 만큼 일반 메모리로 밀려나면서(오프로딩) 속도가 무너지는 겁니다.
resolution: [ 512, 768 ] # VRAM ~10GB
이렇게 바꾸니 스텝당 2.1초. 1500스텝이 50분에 끝났습니다. 얼굴 LoRA 기준으로 품질 차이는 못 느꼈어요.
| 설정 | 스텝당 | 1500스텝 예상 |
|---|---|---|
[512, 768, 1024] |
30~42초 | 약 15시간 |
[512, 768] |
2.1초 | 50분 |
여기서 배운 것: VRAM이 한계에 가까워지면 성능이 서서히 나빠지는 게 아니라, 절벽처럼 떨어집니다.
삽질 ② — GPU 100%인데 한 스텝도 안 넘어간다
위 설정을 고치기 전에 더 황당한 일이 있었어요. GPU 사용률 100%, VRAM 15.9/16GB. 컴퓨터는 전력을 다해 일하는 것처럼 보이는데 진행률이 5스텝에서 10분 넘게 멈춰 있었습니다.
프로세스별로 VRAM을 까봤더니, 범인이 학습이 아니었어요.
python (학습) 14.53 GB
dwm 6.40 GB ← ???
dwm.exe는 데스크톱 창 관리자라는 윈도우 기본 프로세스입니다. 창 그림자나 애니메이션을 그리는 애라 평소엔 0.5~1GB면 충분한데, 6.4GB를 쥐고 있었어요. 오래 켜둔 PC에서 가끔 생기는 메모리 누수라고 합니다.
해결은 한 줄입니다. (관리자 권한 필요, 화면이 1~2초 깜빡하고 프로그램은 안 꺼져요)
Stop-Process -Name dwm -Force
다만 이미 밀려난 학습은 메모리를 되찾아줘도 살아나지 못했습니다. 결국 학습은 처음부터 다시 돌렸어요.
배운 것: VRAM이 모자랄 때 의심할 대상은 내가 띄운 프로그램만이 아닙니다.
삽질 ③ — 윈도우에서 “누가 VRAM 쓰는지”는 nvidia-smi가 못 알려줍니다
같은 사고를 또 겪기 싫어서, 앱 화면에 “지금 누가 VRAM을 얼마나 쓰는지”를 표시하기로 했어요. 그런데 이게 의외로 까다로웠습니다.
그래픽카드 상태를 보는 표준 도구인 nvidia-smi부터 시도했는데:
$ nvidia-smi --query-compute-apps=pid,used_memory --format=csv
1952, [N/A]
14544, [N/A]
전부 [N/A]. 윈도우의 기본 드라이버 방식(WDDM)에서는 VRAM 배분을 OS가 관리해서, nvidia-smi가 프로세스별 사용량을 알 수가 없다고 해요.
답은 윈도우 성능 카운터에 있었습니다.
Get-Counter '\GPU Process Memory(*)\Dedicated Usage'
이건 값이 제대로 나옵니다. 문제는 한 번 부르는 데 1.6초나 걸린다는 것. 몇 초마다 갱신하는 화면에는 못 씁니다. 그래서 파이썬에서 같은 카운터를 직접 읽게 했더니(pywin32의 win32pdh) 0.5초로 줄어서 쓸 만해졌어요.
삽질 ④ — 파이썬(torch)이 알려주는 숫자를 믿었다가
전체 VRAM 사용량 정도는 쉽게 구할 줄 알았어요.
free, total = torch.cuda.mem_get_info()
그런데 학습이 7.9GB를 쓰는 동안에도 이 값은 1.2GB에서 꿈쩍하지 않았습니다. 알고 보니 이 함수는 호출한 프로세스 눈에 보이는 값을 줍니다. 학습은 별도 프로세스로 돌아서 앱 쪽에는 안 보여요. 장치 전체를 보려면 nvidia-smi로 물어야 했습니다. 바꾸고 나니 숫자가 딱 맞아떨어졌어요.
학습 11.5GB + 앱 0.2GB + 그 외 1.8GB = 13.5GB (nvidia-smi: 13875 MiB)
배운 것: 두 곳의 숫자가 안 맞으면, 둘 중 하나는 내가 생각한 걸 재고 있지 않은 겁니다.
LoRA는 몇 스텝이나 돌려야 하나
2D 캐릭터를 사진 7장 × 300스텝으로 학습해봤더니, 결과가 원본 그림체는 하나도 반영 안 된 평범한 실사 얼굴이었어요. 아무것도 학습이 안 된 겁니다. 몇 번을 갈아엎고 경험으로 정리하면:
| 조건 | 결과 |
|---|---|
| 사진 7장 × 300스텝 | 아무것도 학습 안 됨 |
| 사진 16장 × 1000스텝 | 얼굴이 잡히기 시작 |
| 사진 16장 × 1500스텝 | 충분 (1000과 큰 차이 없음) |
이 판단이 가능해진 건 중간 샘플 저장 간격을 500스텝에서 50스텝으로 줄인 뒤였어요. “언제쯤 수렴했는지”가 눈에 보이니까 오히려 일찍 끊을 수 있어서 전체 시간은 줄었습니다.
정리 — 배운 것 일곱 줄
| 배운 것 | 한 줄 |
|---|---|
| VRAM 한계 | 넘으면 느려지는 게 아니라 무너집니다 |
| 학습 해상도 | 1024 버킷 하나가 15배를 가릅니다 |
| 범인 찾기 | 내 프로그램만 VRAM을 쓰는 게 아닙니다 (dwm 누수) |
| 윈도우 GPU | nvidia-smi는 프로세스별 사용량을 못 줍니다 → 성능 카운터 |
| torch | mem_get_info()는 내 프로세스가 보는 값일 뿐입니다 |
| 프로세스 제어 | 실제 연산은 자식 프로세스에 있습니다 |
| LoRA 학습량 | 300스텝은 아무것도 아닙니다. 1000부터 시작하세요 |
만들면서 제일 크게 느낀 건, “느리다”의 원인이 대부분 코드가 아니라 자원 경합이었다는 점이에요. 알고리즘을 고친 적은 한 번도 없습니다. 어디에 뭐가 얼마나 올라가 있는지를 보이게 만든 것이 전부였어요.
이 앱으로 블로그 마스코트 캐릭터도 학습시켜서 뽑고 있는데, “너무 진지하게 나와서 컨셉부터 다시 잡은” 이야기는 다음에 따로 쓸게요.