컨테이너 메모리 제한을 걸어놨는데 장비가 통째로 멈췄다

DGX Spark에서 도커 메모리 제한이 통하지 않아 26B LoRA 학습이 장비를 통째로 멈춘 15시간짜리 사고와 두 겹 방어 기록

하려던 것

어제 저녁, DGX Spark(GB10)에서 google/gemma-4-26B-A4B-it LoRA 학습을 걸어 놓고 잤다. BF16, rank 16이다. 컨테이너에 --memory=100g 제한을 걸어 뒀으니 최악의 경우 컨테이너만 죽을 거라고 생각했다.

오늘 아침에 보니 장비가 통째로 죽어 있었다. SSH도 안 되고 콘솔도 안 먹어서 물리적으로 가서 전원을 껐다 켰다. 복구까지 15시간이 걸렸다.

어떻게 시켰나

원인을 찾을 근거는 있었다. 서버 세운 첫날 붙여 둔 Prometheus가 죽는 과정을 분 단위로 남겨 놨다.

UTC가용 메모리
09:48 학습 시작114 GB
10:03 모델 로드 완료54 GB
10:233.5 GB
10:330.3 GB (swap 진입)
10:38swap 9.7 GB
11:07수집 중단 = hang

시작 30~45분 만이다. 메모리가 마르고, swap으로 밀려나고, swap마저 차니까 커널이 멈췄다. 전형적인 swap death다.

어디서 막혔나

컨테이너 제한이 왜 안 먹었는지가 핵심이었다. 긴 샘플 30건으로 재현 테스트를 돌리면서 컨테이너와 호스트를 동시에 봤다.

보는 곳
컨테이너 working_set7.7 GB
실제 호스트 사용량110 GB

GPU가 시스템 RAM을 99GB 먹는 동안 컨테이너는 7.7GB로 보인다. 이유는 둘이다.

  1. DGX Spark는 CPU와 GPU가 128GB를 공유하는 구조인데, CUDA 통합 메모리 할당이 cgroup 카운트를 우회한다. GPU가 잡은 게 결국 호스트 RAM인데 memory.current에 안 잡힌다.
  2. container_memory_working_set_bytes는 mmap/shared를 빼고 센다. docker stats의 MEM 컬럼이 이 값이다.

그래서 Docker OOM-kill이 발동조차 안 했다. 컨테이너 입장에서는 100GB 한도 중 7.7GB 쓰는 얌전한 프로세스였다.

“다운 시점에도 컨테이너 메모리는 얼마 사용 안 했었네.”

대시보드는 끝까지 초록색이었다. 게다가 사고 전후를 훑다가 모니터링 컨테이너가 초당 60MB를 받고 있는 걸 발견했는데, 파 보니 cAdvisor가 호스트 NIC 전체 값을 컨테이너 값으로 복제해 내보내고 있었다. 지표가 이상하면 시스템을 의심하기 전에 지표를 의심해야 한다.

어떻게 풀었나

먼저 시작 시점을 다시 봤다. 학습 시작 때 가용 메모리가 114GB다. 128GB 장비인데 14GB가 이미 없다. 서빙이 같이 돌고 있었기 때문이다.

--gpu-memory-utilization 0.85

이 설정으로 서빙만으로 통합 메모리의 93%를 점유하고 있었다. 남는 게 6.4GB. 학습을 시작할 때 이미 여유가 없었다.

gpu-memory-utilization을 “GPU 메모리의 몇 %“로 읽으면 0.85는 보수적으로 보인다. 그런데 **통합 메모리 장비에서 그건 시스템 전체의 85%**다. 첫날 “보수적인 값”이라고 적었던 걸 정정한다.

그래서 두 겹으로 막았다.

1차 — 학습 설정. max_seq_length를 16384에서 8192로 캡하고, gradient_checkpointing을 켜고(activation 30~50% 절감), 만 자 넘는 샘플은 학습 셋에서 뺐다. 길이별로 재보니 seq 16,000에서 peak가 110GB를 넘고 8,000이면 80GB다. 26B-A4B는 MoE라 활성 expert 수만큼 activation이 더 붙어서 더 나쁘다. 거칠게는 가중치 × 4~8배가 필요하다 — 26B BF16 가중치가 50GB니 기본 설정으로는 200GB다. 애초에 안 들어가는 계산이었다.

최후 방어선 — earlyoom. 가용 8% 미만 SIGTERM, 4% 미만 SIGKILL. 학습 프로세스를 우선 죽이고 sshd는 보호한다. 커널이 멈추기 전에 뭐라도 죽여야 SSH로 들어갈 수 있다. 15시간을 날린 이유가 그거였다.

그리고 학습 전에 swap을 끈다. 역설적인데, swap이 있으면 죽는 대신 질질 끌다가 커널이 hang된다. swap을 꺼야 OOM 킬러가 빨리 문다.

배운 점