탈락 확정 이틀 뒤, 30등이 되었다
연작 3편에서 41등으로 닫은 대회의 후일담이다. 3편은 그날의 사실로 둔다. 이 편은 그 결말이 뒤집힌 기록이다.
7월 26일에 SNU AI Challenge를 종료 처리했다. Public 0.90052, 130팀 중 41위. 본선 심사 대상인 30위 안에 못 들었으니 끝이었다. 프로젝트 폴더를 보관함으로 옮기고 트래커에 탈락을 기록했다. 3편은 그렇게 끝났다.
27일 저녁, 운영팀 메일이 왔다. 상위 팀 일부가 실격되면서 순위가 재배치됐고, 우리가 Public과 Private 합산 30위 안으로 들어왔다는 내용이었다. 캐글 API로 확인하니 팀 순위가 30으로 찍혀 있었다. 마감은 다음 날 밤 12시. 남은 시간은 23시간 남짓이었다.
내야 할 것은 세 개였다. 보고서, 코드와 가중치가 든 저장소, 그리고 외부 데이터셋에 대한 추론 결과. 마지막 것은 제출 기회가 한 번뿐이고, 제출한 코드로 운영진이 재현했을 때 결과가 다르면 실격이라는 조건이 붙어 있었다.
최고 모델의 가중치가 없었다
문제는 그 세 개 전부가 가중치를 필요로 한다는 거였다. 그리고 최고 기록을 낸 모델의 가중치는 3편에 썼던 그 사고로 이미 없었다.
원인을 이번에 정확히 찾았다. 캐글 데이터셋 업로드 명령의 기본값이 디렉터리를 건어뛰는 설정이다. 어댑터 폴더째 조용히 빠진 채 로그와 CSV만 올라갔고, 아무런 오류도 경고도 없었다. 성공으로 끝난 업로드였다. 그 뒤 인스턴스를 지웠으니 디스크의 원본도 없었다.
여기서 규칙 하나를 새기게 됐다. 업로드의 성공은 종료 코드가 아니라 올라간 파일 목록으로 확인한다. 성공 로그는 파일이 다 올라갔다는 증거가 아니라 도구가 스스로 죽지 않았다는 증거일 뿐이다. 그리고 아티팩트 업로드 도구의 기본값이 "덜 올리는" 쪽일 때, 그 사실은 사고가 나기 전에는 절대 알려주지 않는다.
12시간 반의 재현 학습
가중치를 되살리는 방법은 하나였다. 같은 레시피로 다시 학습하는 것. 시드와 학습률과 프롬프트와 파서 전부 기록에 남아 있으니 재현은 가능했다. 새벽 1시 반에 렌탈 GPU 한 대로 2단계 학습을 시작했다. 예상 착지는 오후 2시 반.
오전에 두 가지가 막혔다가 풀렸다. 렌탈 잔액이 4.86달러로 재현 런의 최악 비용과 정확히 같았고, 외부 데이터는 초대 링크를 수락해야 다운로드가 열리는 구조였다. 둘 다 내 권한 밖이라 아침까지 기다렸다. 잔액이 22달러가 되고 초대가 수락되면서 길이 열렸다.
학습은 예상보다 느렸다. 잡힌 4090이 원래 쓰던 것보다 샘플당 30% 느린 개체였다. 같은 시간 제한이면 원래 레시피보다 25% 적게 학습한다. 여기서 선택이 있었다. 시간을 맞출 것인가, 학습량을 맞출 것인가. 레시피의 정체는 시간이 아니라 에폭 예산이라고 판단해서, 1단계가 끝난 뒤 같은 학습률로 한 단계를 더 돌렸다. 검증 점수는 1단계 0.4933, 2단계 0.6000, 3단계 0.6233으로 올라갔다. 원본의 0.6367에는 0.013 모자랐다. 재현으로 회수 가능한 범위 안이었다.
외부 데이터는 우리가 약하다고 적어둔 바로 그 분포였다
외부 검증 데이터를 열어보니 3,884행이었다. 캡션이 학습 데이터와 완전히 다른 계열이었다. 학습 데이터는 카메라워크를 서술한 긴 문장으로 중앙값 26단어였는데, 이 세트는 "무언가를 오른쪽에서 왼쪽으로 민다" 같은 액션 템플릿 104종이 반복되는 중앙값 5단어짜리였다.
우리 오류분석이 이미 짚어둔 급소다. 15단어 이하 캡션에서 정확도가 0.276으로, 긴 캡션의 0.699와 두 배 이상 차이가 났다. 이 세트에서 점수가 낮게 나오는 건 사고가 아니라 예정된 결과다. 그래서 보고서에 결과를 보기 전에 먼저 적었다. 이 모델의 약점은 캡션 길이라고, 짧은 템플릿으로 만든 평가 세트에서는 리더보드 수치보다 훨씬 낮게 나올 거라고. 결과를 보고 다는 변명과, 결과 전에 적는 예측은 다른 물건이다.
제출은 밤 10시 9분에 끝났다. 점수는 0.50이었다. 우리가 인도메인에서 측정한 짧은 캡션 약점 구간 0.276보다 오히려 높았다. 어려운 분포에서도 레시피 자체는 일반화가 됐다는 뜻으로 읽는다.
인스턴스 세 대가 연달아 죽었다
추론은 3,884행을 네 조각으로 나눠 네 대에서 병렬로 돌렸다. 그중 세 대가 각기 다른 방식으로 죽었다.
첫 번째는 네트워크가 초당 500킬로바이트로 조여진 호스트였다. 두 번째와 세 번째는 모델 다운로드가 중간에 멈춘 호스트들이었다. 네 번째는 컨테이너 자체가 플랫폼 단에서 죽어 있었다. 전부 파괴하고 다시 잡았다. 마지막에는 살아남은 한 대에 나머지 두 조각을 순차로 돌려서 마쳤다.
이날 가장 어이없던 사고는 따로 있었다. 2단계 가중치가 저장되는 오후 2시 반에 회수와 다음 발사를 자동으로 하려고 감시 스크립트를 걸어뒀는데, 정작 그 시각에 아무 일도 안 일어났다. 원인은 스케줄러가 대화형 세션의 PATH를 물려받지 않는다는 것이었다. 명령어를 못 찾은 스크립트가 빈 문자열을 계속 받아들이며 "아직 안 나옴"만 몇 시간째 반복하고 있었다. 백그라운드로 돌리는 스크립트는 PATH를 자기가 명시한다. 조용히 실패하는 감시자는 없는 감시자보다 나쁘다.
종결 처리는 되돌림 비용을 남기고 한다
이번 건에서 가장 오래 남을 교훈은 기술이 아니다. 7월 26일의 종료 판정은 그 시점의 사실에 맞았다. 틀린 건 판정이 아니라, 종결 처리를 할 때 되돌림 비용을 영으로 가정한 것이다.
대회 결과는 발표 후에도 움직인다. 실격 재배치가 이번처럼 순위를 뒤집을 수 있고, 이의신청으로 바뀌는 경우도 있다. 폴더를 보관함으로 옮기고 기록을 닫는 건 결과가 바뀔 창이 닫힌 뒤의 일이다. 다행히 학습 스크립트와 저널과 원본 아카이브는 전부 남아 있어서 23시간짜리 부활이 가능했다. 기록을 지우지 않은 것이 이 부활의 전제였다.
남은 것
보고서와 코드와 추론 결과는 마감 안에 전부 들어갔다. 이제 기다리는 건 본선 진출 선정 통보다. 성능과 보고서를 합쳐 열 팀 안팎을 뽑는다.
3편에서 나는 41등의 원인이 첫날에 안 한 리서치라고 썼다. 그 판단은 그대로다. 다만 그날 알지 못했던 게 하나 있다. 41등은 끝이 아니라 대기번호였다는 것. 끝이라고 생각한 날에도 기록을 남기고 도구를 정리하둔 게, 이틀 뒤의 23시간을 가능하게 했다. 다음에도 종결 처리는 천천히 한다.