~13 min · inference-mode, autograd, memory, oom, war-story
Level 0툴 임차인
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"2 GB 써야 할 forward 전용 경로가 75 를 썼어. 모델이 너무 큰 게 아니었어. framework 가 조용히 숙제를 저장하고 있었어."
말이 안 되던 크래시
수수한 이미지 모델이, 이미지 하나 생성하는데, 머신을 끌어내렸어 — 두 번. 숫자가 터무니없었어: 약 2 GB 메모리에 맞아야 할 경로가 display server 자체가 쓰러지기 전에 대략 75 GB 로 부풀었어. 모델 크기로는 아무것도 설명 안 됐어. 모델이 작았어. 이미지가 작았고. 뭔가 안 보이는 게 메모리를 수십 기가바이트씩 먹고 있었어.
네가 안 볼 때 autograd 가 하는 것
범인은 automatic differentiation 엔진이었어. 기본적으로, 딥러닝 framework 는 네가 훈련하고 싶을 수도 있다고 가정해 — 그래서 모든 forward pass 마다, 모든 레이어의 중간 activation 을 조용히 유지해, backward pass 중 gradient 계산에 필요할 거니까. 훈련엔 그 유지가 필수야. inference 엔, backward 를 절대 안 부르니까, 순수 죽은 무게고 — timestep 많은 깊은 diffusion 모델에선, 그 죽은 무게가 아무도 안 쓸 activation 수십 기가바이트로 쌓여.
framework 의 기본값은 가장 까다로운 use case 를 가정해. 딥러닝 framework 는 '이걸 훈련할 수도' 를 기본값으로 해, 훈련이 가장 많은 bookkeeping 이 필요하니까. inference 전용 경로에선, 그 기본값이 안 보이는 낭비야. 도구가 기본적으로 뭘 가정하는지 알아, 기본값이 네 특정 경로에 맞춰진 경우가 드무니까.
한 줄 해법
해법은 framework 한테 명시적으로, 이 경로가 gradient 가 절대 필요 없을 거라고 말하는 거야: forward pass 를 inference/no-grad context 로 감싸. 그러면 엔진이 activation 유지를 멈추고, 75 GB 가 작업이 실제로 요구하는 ~2 GB 로 도로 무너져. context manager 하나, 자릿수만큼의 메모리 차이. 버그는 모델이나 수학에 없었어 — 의도 선언이 빠진 거였어.
가장 큰 승리는 자주 최적화가 아니라 선언이야. 알고리즘 안 바뀜. 모델 안 줄음. 단일 문장이 framework 한테 코드가 실제로 뭘 하는지 말했고, framework 가 해당 안 되는 케이스에 비싼 작업을 멈췄어. 도구한테 네 의도를 말하는 게 가끔 어떤 똑똑한 최적화보다 가치 있어.
왜 머신 전체를 끌어내렸는지
unified-memory Apple Silicon 시스템에선, GPU 랑 시스템 메모리가 한 풀을 공유해. 그래서 폭주하는 inference 할당이 Python 프로세스만 크래시 안 시켜 — 운영체제를 굶기고, display server(네 화면 그리는 것)가 같이 내려가. 그래서 실패가 그렇게 극적이었어: 실수를 흡수할 별도 VRAM 이 없어. unified memory 는 큰 모델엔 멋지고 뭔가 새면 가차없어.
공유 자원은 지역적 버그를 전역 크래시로 만들어. GPU 랑 OS 가 같은 메모리 풀에서 끌어 쓰면, inference 누수가 격리 안 돼 — 모든 걸 끌어내려. 공유 자원 시스템에선, 다른 데선 격리된 크래시일 버그가 전체-시스템 실패가 돼. 공유 자원을 머신 전체가 거기 의존하는 것처럼 예산 잡아, 실제로 그러니까.
피파의 고백
난 inference_mode 를 백 번 읽고 있으면 좋은 것, 작은 속도 향상으로 다뤘어. 그게 머신 죽이는 75 GB 를 차분한 2 GB 로 되돌리는 걸 보는 게 그걸 다시 배선했어. 최적화가 아니야 — 돌아가는 엔진이랑 주기적으로 컴퓨터 전체를 죽이는 엔진의 차이야. 어떤 한 줄은 광택이 아니라; 하중을 지탱해. 이 뒤로 난 '지루한' context manager 를 훑고 지나가길 멈췄어.
Code
decorator 하나, 자릿수만큼·python
import torch
# 선언 없이: autograd 가 backward() 부를 '혹시 몰라' 모든 activation 유지.
# timestep 많은 깊은 diffusion forward pass 에선, 그 유지가 수십 GB 로
# 쌓여. ~2 GB 실제 작업 -> ~75 GB 사용.
def infer_leaky(model, latents, conditioning, steps):
x = latents
for t in range(steps):
x = model.denoise(x, t, conditioning) # step 마다 activation 유지
return x
# 선언 있이: framework 한테 이 경로가 gradient 절대 필요 없다고 말해.
# activation 안 유지됨. 75 GB 가 ~2 GB 로 도로 무너져.
@torch.inference_mode() # 하중 지탱하는 한 줄
def infer_clean(model, latents, conditioning, steps):
x = latents
for t in range(steps):
x = model.denoise(x, t, conditioning)
return x
까먹을 수 없는 데서 guard·python
# guard 를 모듈별이 아니라 ADAPTER 진입점에 둬.
# 자기 no_grad 까먹는 새 모델 family 도 여전히 커버됨.
class LocalAdapter(Adapter):
@torch.inference_mode() # 모든 family 커버, 현재 + 미래
def _infer_sync(self, request):
backbone = self.load(request.model)
return self.sampler.sample(backbone, request)
# 모듈별 no_grad 도 남음 -> 둘-중-둘째 net (심층 방어)
어떤 ML 코드(네 거나 예시)에서 inference 경로를 찾아 no-grad / inference mode 를 선언하는지 확인해. 하면, 머릿속에서 제거하고 뭐가 누적될지 추론해. 안 하면, 그게 잠복 메모리 폭탄이야. 어느 쪽이든, 그 선언이 뭘 사주는지 한 문장으로 적어.
Hint
답할 질문은 늘 '이 경로가 backward 를 언젠가 부르나?' 야. 답이 아니오면, gradient bookkeeping 은 순수 낭비고 그걸 끄는 선언이 선택이 아니라 필수야.
Progress
Progress is local-only — sign in to sync across devices.