내 컴퓨터가 스스로 재부팅된 밤
어느 날 밤, 긴 작업이 한창 돌아가던 도중에 컴퓨터 전체가 스스로 꺼졌다가 다시 켜졌다. 프로그램이 아니라 — 기계가. 한순간 프로세스가 돌아가고 있었는데, 다음 순간 나는 깨끗한 로그인 화면을 바라보고 있었다. 마치 누군가 전원 코드를 뽑았다가 다시 꽂은 것처럼.
나는 며칠 동안 내가 중요하다고 확신했던 단 하나의 자원에 대해 신중을 기했다. 그 충돌은 내가 지켜볼 생각조차 해본 적 없는 자원에서 왔다.
메시지 없는 충돌
가장 불안했던 부분은 읽을 거리가 너무 적었다는 점이다. 스택 트레이스도, 예외도, 끝을 표시하는 로그 줄도 없었다. 그저 갑작스러운 재부팅과, 운영체제 자체의 기록 속에 묻혀 있는, 디지털판 어깨 으쓱임에 해당하는 한 줄뿐이었다: 시스템이 정상적으로 종료되지 않은 채 다시 시작되었습니다.
평범한 버그는 시신을 남긴다. 이 버그는 텅 빈 방을 남겼다. 그리고 메시지 없는 에러는 그 자체로 하나의 단서다 — 그것은 보통 실패가 당신의 프로그램 아래 어딘가에서, 모든 것을 끌어내리며 무너지기 전에 멈춰 서서 설명해 주지 않는 어떤 계층에서 일어났음을 뜻한다.
나는 잘못된 메모리를 예산으로 잡았다
나는 매우 큰 배치 작업을 GPU에서 돌리고 있었고, 그 배치 크기를 신중하게 잡았다 — GPU의 메모리를 기준으로. 카드에 VRAM이 얼마나 있는지 알았고, 각 작업 단위가 대략 얼마를 차지할지 알았으며, 한계선 아래에 안전하게 머무르도록 산수를 해뒀다. 내가 보기엔 메모리는 해결된 문제였다.
그것은 잘못된 메모리에 대해 해결된 것이었다. 카드의 VRAM은 내내 멀쩡했다; 충돌이 일어난 순간에 그 계기판을 보고 있었다면 완벽하게 평온해 보였을 것이다. 내가 실제로 부딪힌 한계선은 호스트 머신의 시스템 RAM — 컴퓨터의 나머지 부분이 돌아가는 평범한 메모리 — 이었고, 나는 그것을 전혀 예산으로 잡지 않았다. 내 머릿속 모델에서 그것은 애초에 제약 조건이 아니었기 때문이다.
호스트 RAM이 어디로 갔나
거대한 배치 작업을 GPU로 보내는 것은 호스트 쪽에서 공짜가 아니다. GPU가 무언가를 실행하기 전에, 작업은 준비되고, 세팅되고, 넘겨져야 한다 — 그리고 그 준비는 시스템 RAM 안에 산다. 그 세팅의 각 조각은 그 자체로는 소박했다. GPU를 포화시킬 만큼 큰 배치에 걸쳐 곱해지자, 그 소박한 조각들이 쌓여 거대한 무언가가 되었다.
시스템 RAM이 가득 찼다. 운영체제는 메모리가 부족할 때 하는 일을 했다 — 디스크로 흘려보냈고, 그러다 그것마저 모자라게 되었다. 심각한 메모리 압박이 항상 잡아낼 수 있는 예외로 스스로를 알리는 것은 아니다. 그것은 실패한 할당을, 죽임당한 프로세스를, 혹은 기계 전체가 불안정해지는 것을 뜻할 수 있다. 내 것은 재부팅할 만큼 세게 불안정해졌다.
왜 나는 그것이 다가오는 걸 못 봤나
돌이켜보면 답답한 점은 내가 실제로 신중하게 모니터링하고 있었다는 것이다 — 그저 잘못된 계기를 모니터링했을 뿐이다. 내 눈은 GPU에, 내가 최적화하고 걱정하고 스스로 영리하다고 느꼈던 그 자원에 가 있었다. 그 내내, 압박은 내가 들여다볼 이유가 없던 곳에서 쌓이고 있었다. 나는 이미 그곳이 문제가 생길 곳이 아니라고 결론 내려 두었기 때문이다.
이것은 이제 너무 여러 번 봐서 우연이라고 부를 수 없는 패턴이다: 당신은 지금 생각하고 있는 제약에 계측을 달고, 생각하고 있지 않은 제약에 대해서는 눈이 먼다. 버그는 숨지 않는다. 그것은 한계가 어디 있는지에 대해 자신만만했던 탓에, 당신이 스스로 만들어낸 사각지대에 앉아 있다.
해결책: 벽을 예상하고, 거기서 튕겨 나오라
수리는 더 나은 계산이 아니었다. 그것은 안전한 크기를 미리 계산해낼 수 있다는 발상 자체를 포기하는 것이었다. 기계가 견딜 수 있는 배치가 정확히 얼마나 큰지 예측하는 대신, 나는 기계가 내게 말하게 했다: 배치를 시도하고, 복구 가능한 메모리 에러에 부딪히면, 그것을 물러서라는 신호로 취급한다 — 작업량을 보수적으로 줄이고 재시도하며, 들어맞을 때까지 축소한다.
그것은 겸손한 작은 루프이고, 내 신중한 산수보다 훨씬 견고하다. 바로 그것이 모든 숨은 비용을 미리 이해해 두었는지에 의존하지 않기 때문이다. 나는 이제 벽이 정확히 어디 있는지 알 필요가 없다; 복구 가능한 메모리 에러에 대해서는, 프로세스가 어떤 하드웨어에서 돌든 더듬어가며 작동 가능한 크기를 찾아낼 수 있다. 그날 밤 같은 호스트 수준의 고갈은 더 어려운 경우다 — 그것은 여전히 단순한 재시도 루프가 아니라 외부의 한계와 모니터링을 필요로 한다.
자원 가정은 그렇지 않게 되기 전까지 조용하다
내가 간직한 교훈은 GPU 그 자체와는 별로 상관이 없다. 그것은 자원 가정이 파국적이 되는 그 순간까지는 보이지 않는다는 것이다. 당신은 머릿속에 그리고 있는 한계에 대해 정확히 옳으면서도 그리고 있지 않은 한계에 의해 무너질 수 있다 — 그리고 그 실패는, 닥쳐올 때, 흔히 아무런 설명 없이 도착한다. 무너지는 계층이 당신의 에러 메시지가 사는 계층 아래에 있기 때문이다.
그래서 나는 자원에 대해 정밀하려는 시도를 멈추고, 그것에 대해 방어적이려고 애쓰기 시작했다. 정밀함은 당신이 모든 비용을 안다고 가정한다. 방어성은 당신이 최소한 하나는 놓쳤다고 가정한다. 무인으로, 당신이 완전히 통제하지 못하는 메모리 위에서, 몇 시간 동안 돌아가야 하는 시스템에서는, 두 번째 가정만이 유일하게 안전한 것이다.
충돌하는 프로그램은 당신에게 메시지를 남긴다. 재부팅하는 기계는 당신에게 침묵을 남긴다. 나는 그 침묵을, 내 하드웨어가 내게 들려준 것 중 더 정직한 축에 드는 것으로 여기게 되었다: 너는 잘못된 것을 예산으로 잡고 있었어.
— 신호도, 수익도 없으며, 투자 자문이 아닙니다.