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부터 시작하세요

만들면서 제일 크게 느낀 건, “느리다”의 원인이 대부분 코드가 아니라 자원 경합이었다는 점이에요. 알고리즘을 고친 적은 한 번도 없습니다. 어디에 뭐가 얼마나 올라가 있는지를 보이게 만든 것이 전부였어요.

이 앱으로 블로그 마스코트 캐릭터도 학습시켜서 뽑고 있는데, “너무 진지하게 나와서 컨셉부터 다시 잡은” 이야기는 다음에 따로 쓸게요.