콘텐츠로 이동
Search

Pointgroup:TrainingEvaluationOOM

훈련중 평가(Evaluation)를 실행하면 OOM이 발생하는 이슈

  • PointGroup 학습 exit 137 진단과 수정
  • 작성일: 2026-10-01
  • 대상: mlops 플랫폼의 Pointcept 학습 런타임(PointGroup, pg-ptv3)과 Kubernetes 로그 수집
  • 관련 문서: docs/plans/54_training_pod_memory_and_log_follow_plan.md
  • 상태: 코드 수정 완료, 커밋·배포·런타임 이미지 재게시·재학습 실측은 아직 하지 않음

요약

PointGroup 인스턴스 분할 학습이 약 16시간 뒤 종료 코드 137 로 끝났다. 원인은 두 가지가 겹친 것이다.

  1. 평가 단계가 쓰는 메모리가 학습이 진행될수록 커진다. 평가기가 제안마다 원본 점 수 크기의 마스크를 만들어 모든 검증 씬이 끝날 때까지 붙들기 때문이다.
  2. 검증 데이터 로더가 평가 때마다 워커 프로세스를 새로 fork 한다. 이미 커진 메인 프로세스를 fork 하면 그 메모리가 사본으로 한 벌 더 생긴다.

둘의 합이 파드 메모리 상한 64Gi 를 넘어 커널이 파드를 죽였다. 진단 중에 별개의 결함도 드러났다. 플랫폼의 로그 수집이 파드 시작 4시간 뒤에 끊기고 다시 붙지 않아서, 실패 화면이 12시간 묵은 로그를 보여 주고 있었다.

GPU 통합 메모리, /dev/shm, 배치 크기는 원인이 아니었다.

증상

학습 화면에 다음 메시지가 표시되었다.

학습이 실패했습니다 (exit 137). 학습 단계에서 멈췄습니다
메모리 부족(OOM)으로 종료되었습니다. 배치 크기나 입력 크기를 줄이거나 /dev/shm 을 키우세요.
파드 메모리 상한(GEO_MLOPS_TRAINING_RUNTIME_MEMORY_LIMIT)에 걸렸을 수도 있습니다.
trainer exited with code 137:
[2026-09-30 08:47:35,902 INFO misc.py line 117 1] Train: [8/100][310/494] ... loss: 4.4757 seg_loss: 0.0000 ...

메시지만 보면 8 epoch 의 학습 스텝 도중에 죽은 것처럼 보인다. 실제로는 그렇지 않았다. 아래 타임라인 참고.

seg_loss: 0.0000 은 결함이 아니다. 클래스를 고르지 않은 실행은 단일 클래스 object 로 학습하고, 클래스가 하나면 교차 엔트로피가 상수 0 이다. 학습은 오프셋 손실이 이끈다.

진단

처음 세운 가설

가설

결과

파드 메모리 상한(전날 추가, 기본 64Gi)에 걸렸다

맞음

GPU 통합 메모리가 파드 cgroup 에 과금되어 상한이 CPU 와 GPU 합산이 되었다

아님. cgroup 의 kernel 몫이 0.29GB 뿐

/dev/shm 이 부족하다

아님. shmem 0.24GB

호스트 메모리가 epoch 를 거치며 자란다

맞음. 다만 학습 루프가 아니라 평가 단계에서

노드에서 확인한 값

배포 노드에서 커널 로그를 읽었다.

sudo dmesg -T | grep -i -B2 -A6 "out of memory"
sudo dmesg -T | grep -B45 -A3 "oom-kill:constraint"
sudo dmesg -T | grep -A45 "Memory cgroup stats for"

핵심 줄은 다음과 같다.

oom-kill:constraint=CONSTRAINT_MEMCG, ... task=python,pid=442561
Memory cgroup out of memory: Killed process 442561 (python) total-vm:114837024kB, anon-rss:36670684kB, file-rss:95364kB, shmem-rss:231896kB
Tasks in ... are going to be killed due to memory.oom.group set
Memory cgroup out of memory: Killed process 443443 (pt_data_worker) total-vm:21504120kB, anon-rss:1190224kB
Memory cgroup out of memory: Killed process 1672569 (pt_data_worker) total-vm:108775600kB, anon-rss:31221212kB

Memory cgroup stats for ...:
anon 68195393536
file 237563904
kernel 286511104
shmem 237547520
inactive_anon 57443008512
active_anon 10989826048

항목

값

뜻

파드 상한

64Gi (67.1GB)

GEO_MLOPS_TRAINING_RUNTIME_MEMORY_LIMIT 기본값

cgroup anon

68.2GB

상한을 채운 것은 전부 프로세스 익명 메모리

cgroup kernel

0.29GB

GPU 드라이버 메모리는 cgroup 에 잡히지 않음

cgroup shmem

0.24GB

/dev/shm 은 무관

메인 python

36.7GB

평가가 쥔 메모리

늦게 fork 된 워커 (pid 1672569)

31.2GB

부모 메모리의 사본

시작 때 fork 된 워커 (pid 443443)

1.2GB

학습 로더 워커

숫자를 읽는 법

  • constraint=CONSTRAINT_MEMCG 는 노드 전체가 아니라 파드 cgroup 상한에 걸렸다는 뜻이다.
  • fork 된 프로세스의 RSS 는 부모와 공유하는 페이지까지 포함한다. 그래서 워커의 31.2GB 가 공유분인지 자기 몫인지는 프로세스 표만으로는 알 수 없다.
  • cgroup 은 페이지를 한 번만 센다. 세 프로세스의 익명 메모리 합(69.1GB)이 cgroup anon(68.2GB)과 거의 같으므로 워커의 31.2GB 는 공유분이 아니라 자기 소유의 사본이다.
  • 이 사본이 없었다면 사용량은 38GB 쯤으로 상한에 한참 못 미쳤다.
  • 워커가 둘뿐이고 pid 가 크게 다르다. 하나는 학습 시작 때, 다른 하나는 한참 뒤에 만들어졌다. 뒤의 것이 평가 때 새로 fork 된 검증 로더 워커다.
  • inactive_anon 이 57GB 다. 대부분의 메모리가 최근에 쓰이지 않았다. 워커가 들고 있던 사본과 메인 프로세스가 붙들고만 있던 마스크가 여기에 해당한다.

원인

평가 메모리가 epoch 를 따라 자란다

PointGroup 은 추론 때 점들을 군집으로 묶어 제안을 만든다. 평가 경로는 다음과 같다.

  1. 모델이 [제안 수, 복셀 수] 크기의 int32 조밀 텐서를 CPU 에 만든다.
  2. Pointcept 의 InsSegEvaluator.eval 이 이를 pred_masks[:, idx] 로 원본 점 수만큼 펼친다. [제안 수, 원본 점 수] int32 텐서가 된다.
  3. associate_instances 가 제안마다 원본 점 수 크기의 bool 마스크를 만들어 pred_inst["mask"] 에 넣는다. 이 마스크는 모든 검증 씬이 끝나 집계할 때까지 살아 있다.
  4. 집계(evaluate_matches)는 교집합 수와 점 수만 읽는다. 마스크는 읽지 않는다.

제안 수는 모델이 덩어리를 잘 묶을수록 늘어난다. 단일 클래스 실행에서는 제안 점수가 늘 1.0 이라 걸러지는 제안도 없다. 그래서 평가 메모리가 epoch 를 따라 커진다. 초반 평가는 무사히 지나가고 뒤 epoch 에서야 상한에 닿는 이유다.

검증 워커 fork 가 메모리를 복제한다

Pointcept 은 학습 로더를 persistent_workers=True 로 만든다. 워커는 학습 시작 때 한 번만 Fork 된다. 그 시점의 메인 프로세스는 작다.

검증 로더에는 이 옵션이 없다. <span style="color: red">평가 때마다 워커를 새로 Fork 한다. Fork 된 워커는 그 시점 부모의 메모리를 통째로 물려받는다. 처음에는 공유 상태지만, 부모가 평가를 돌며 그 메모리를 다시 쓰면 copy-on-write 로 부모는 새 페이지를 받고 워커는 옛 페이지를 혼자 갖게 된다. 결과적으로 메모리가 두 벌이 된다.

로그 수집이 4시간에 끊긴다

서버의 follow_job_logs 는 파드 로그 스트림을 한 번만 열었다. 스트림이 끝나면 그대로 반환하고 다시 붙지 않았다. 스트림은 두 경우에 오류 없이 조용히 끝난다.

  • kubelet 이 follow 연결을 스트리밍 제한 시간(기본 4시간)에 닫는다.
  • HTTP 클라이언트가 한 시간 동안 데이터가 없으면 읽기 시간 초과로 끝낸다.

이 실행은 파드가 뜬 지 정확히 4시간 되는 순간에 로그가 멎었다.

타임라인

시각 (KST)

일

09-30 13:47:35

학습 파드 시작

09-30 17:47:35

플랫폼이 수집한 마지막 로그 줄 (8 epoch 310번째 스텝). 파드 시작 뒤 정확히 4시간

09-30 17:47 부터

학습은 계속되었으나 로그는 수집되지 않음

10-01 05:45:15

memcg OOM 으로 파드 종료. 실행 시간으로 역산하면 30 epoch 근처의 평가 도중

컨테이너 로그의 시각은 UTC 이고 노드의 dmesg -T 는 KST 다. 둘을 맞춰 보면 마지막 로그와 종료 사이에 12시간의 공백이 있다.

수정

네 가지를 Phase 로 나눠 구현했다.

Phase 1. 평가기가 마스크를 붙들지 않는다

  • 파일: src/geo_mlops/resources/training_runtime/pointcept_insseg.py
  • associate_instances 함수를 새로 두었다. 제안 하나의 마스크를 그때그때 만들어 점 수를 세고 버린다. 원본 점으로 옮길 때도 제안 한 줄씩만 옮긴다. [제안 수, 원본 점 수] 텐서는 만들지 않는다.
  • 정답 인스턴스와의 교집합은 제안이 덮은 점의 인스턴스 ID 를 한 번 세어 얻는다. 원본처럼 정답 인스턴스마다 전체 점을 다시 훑지 않으므로 평가도 빨라진다.
  • GeoInsSegEvaluator 가 Pointcept 의 InsSegEvaluator 를 물려받아 eval 만 바꾼다. AP 계산과 best 체크포인트 기준(AP50)은 그대로다.
  • 생성 설정이 부르는 평가기 이름이 GeoInsSegEvaluator 로 바뀌었다.

Phase 2. 검증 로더는 워커 없이 읽는다

  • 파일: src/geo_mlops/resources/training_runtime/pointcept_semseg.py
  • GeoTrainer.build_val_loader 가 워커 수를 VAL_LOADER_WORKERS = 0 으로 두고 로더를 만든다. 평가 때 fork 가 일어나지 않는다.
  • 검증은 배치 1 이고 씬 수가 적어 워커가 벌어 주는 시간이 거의 없다.
  • 시맨틱 분할과 인스턴스 분할 모두에 적용된다. 학습 로더의 워커 수(num_workers)는 그대로다.

Phase 3. 로그 팔로워가 재접속한다

  • 파일: src/geo_mlops/integrations/kubernetes.py
  • 스트림이 끝나면 파드 상태를 묻는다.
    • 실행 중이면 마지막으로 받은 줄의 시각부터 다시 붙는다.
    • 끝났으면 꼬리를 줍기 위해 한 번 더 읽고 마친다.
    • 파드가 사라졌으면 마친다.
    • 클러스터를 읽을 수 없으면 실행 중으로 보고 다시 시도한다.
  • 로그를 타임스탬프와 함께 요청한다. _LogCursor 가 재접속 때 다시 오는 줄을 버리고, 타임스탬프를 뗀 줄만 넘긴다. 같은 타임스탬프의 줄이 여럿이어도 잃지 않는다.
  • 재접속 간격은 2초에서 시작해 연속 실패 때 30초까지 늘어난다.
  • 4시간을 넘는 모든 학습에 해당하던 결함이다. PointGroup 에 한정된 문제가 아니다.

Phase 4. epoch 마다 메모리를 기록한다

  • 파일: src/geo_mlops/resources/training_runtime/pointcept_semseg.py
  • GeoProgress.after_epoch 가 epoch 마다 한 줄을 찍는다.
-> memory epoch 30: rss 3.00GiB (peak 36.00GiB), pod 5.00GiB of 64.00GiB (peak 40.00GiB), cuda reserved 8.00GiB (peak allocated 6.00GiB)

항목

뜻

rss

메인 프로세스의 상주 메모리. 평가가 끝난 뒤의 값

rss 의 peak

지난 보고 뒤의 최고치. 평가가 epoch 끝에 돌므로 그 epoch 평가의 크기

pod

파드 cgroup 전체 사용량과 상한. 상한에 걸리는 값이 이것

pod 의 peak

파드 시작 뒤의 최고치

cuda

PyTorch 가 예약한 GPU 메모리와 최대 할당량

exit 137 은 사후에 원인을 말해 주지 않는다. 이 줄이 있으면 어느 값이 언제부터 자랐는지를 노드에 들어가지 않고 읽을 수 있다.

손대지 않은 것

  • 모델 안의 [제안 수, 복셀 수] int32 텐서는 Pointcept 포크의 코드라 그대로 두었다. 복셀 기준이라 원본 점 기준 텐서보다 훨씬 작다. Phase 4 의 기록에서 평가 최고치가 여전히 크면 그때 고친다.
  • 실패 메시지 문구와 메모리 상한 기본값(64Gi)은 바꾸지 않았다.

검증

대상

방법

결과

연관 계산 (Phase 1)

Pointcept 구현을 옮긴 기준 구현과 대조 (무작위 씬 5개, 무시 클래스 설정 2가지)

일치

마스크 미보관 (Phase 1)

반환값 어디에도 배열이 남지 않는지 확인

통과

검증 로더 (Phase 2)

트레이너 소스에서 재정의와 원복 확인

통과

로그 재접속 (Phase 3)

닫힌 스트림, 끊긴 스트림, 클러스터 조회 실패, 파드 삭제, 중복 줄 제거

통과

메모리 보고 (Phase 4)

가짜 proc·cgroup 파일로 출력 확인, 파일이 없을 때도 실패하지 않음

통과

회귀

tester/kubernetes, tester/training 전체

889 통과, 1 실패

실패 1건은 test_flash_attention_stays_on 이다. 개발 환경에 flash-attn 이 없어 나는 실패이고 이번 변경과 무관하다.

검증하지 못한 것이 하나 있다. GeoInsSegEvaluator.eval 본문은 개발 환경에 torch 와 pointcept 가 없어 실행해 보지 못했다. 연관 계산은 대조했지만 평가 루프 자체는 재학습 실측에서 처음 돈다.

남은 일

  1. 변경을 커밋한다.
  2. 서버를 배포한다. Phase 3 은 서버 코드라 이것만으로 적용된다.
  3. Pointcept 학습 런타임 이미지를 재게시한다. Phase 1, 2, 4 는 이미지 안의 트레이너 코드다.
  4. 같은 데이터로 pg-ptv3 를 다시 학습한다. 확인할 것은 세 가지다.
    • 첫 평가가 정상적으로 끝나고 AP 값이 나오는가
    • 4시간 뒤에도 로그가 이어지는가
    • -> memory 줄의 peak 가 epoch 를 따라 얼마나 자라는가

같은 증상을 다시 만났을 때

노드에서 먼저 커널 로그를 읽는다.

sudo dmesg -T | grep -A45 "Memory cgroup stats for"
sudo dmesg -T | grep -B10 -A6 "oom-kill:constraint"

그다음 아래 순서로 읽는다.

  1. 실패 메시지의 로그 꼬리가 가리키는 epoch 을 그대로 믿지 않는다. 로그의 -> memory 줄과 MLflow 의 마지막 step 을 먼저 본다.
  2. constraint 가 CONSTRAINT_MEMCG 인지 본다. 그렇다면 노드 전체가 아니라 파드 상한에 걸린 것이다.
  3. cgroup 통계에서 anon, shmem, kernel 중 무엇이 큰지 본다.
    • anon 이 크면 프로세스 힙이다.
    • shmem 이 크면 /dev/shm 이다.
    • kernel 이 크면 드라이버나 커널 쪽이다.
  4. 프로세스 표의 익명 메모리 합을 cgroup anon 과 견준다. 합이 훨씬 크면 fork 로 공유된 페이지가 이중으로 세어진 것이고, 비슷하면 각자 자기 몫을 들고 있는 것이다.
  5. 추세가 필요하면 모니터링에서 파드의 container_memory_working_set_bytes 를 본다.

See also

lastmod 2026-10-01created 2026-10-01author Zer0 sitemap