링을 p4 위로 옮기기
엔진과 그 호출자 사이에서 어긋난 계약 일곱 가지. 각각 다르게 실패했고, 그중 오류처럼 보인 것은 하나뿐이었다.
p4 엔진의 새 릴리스를 머지했더니 링이 서빙을 멈췄다. 크래시가 난 것도 아니었다 — 로더는 성공을 보고했고, 에이전트들은 ready를 보고했고, 아무 일도 일어나지 않았다. 그 침묵에서 거슬러 올라가는 데 하루가 걸렸고, 우리 호출자와 엔진이 서로 어긋나 있던 곳을 일곱 군데 찾아냈다. 적어 둘 가치가 있는 이유는 개수가 아니다. 일곱 중 여섯이 오류를 내지 않았다는 점이다.
존재하지 않는 노드로 향한 이벤트는 거부되지 않고 전달된다
우리 로더는 LOAD 명령을 자기가 만들려는 바로 그 노드 앞으로 보냈다. 하지만 노드는 LOAD가 만들기 전까지 존재하지 않고, 알 수 없는 노드를 지목한 이벤트에 대한 브로커의 규칙은 거부가 아니라 바깥으로 전달이다. 명령은 갈 곳을 찾아 에이전트를 떠났고, 아무 데도 찾지 못했고, 버려졌다. 로그 한 줄 남지 않았다 — 브로커 입장에서는 잘못된 일이 하나도 없었으니까.
수정은 LOAD를 *에이전트* 앞으로 보내되 엔진의 백엔드 중립 라이프사이클 봉투에 싸고, 어댑터 자신의 명령은 불투명한 본문으로 담는 것이었다. 지나고 보면 당연하고, 바깥에서는 보이지 않는다.
다른 숫자와 반드시 같아야 하는 숫자
모델은 load generation 아래 로드되고, 각 노드는 node generation으로 등록된다. 우리는 이 둘을 독립적인 값으로 다루고 있었고 — 하나는 타임스탬프, 다른 하나는 1 — 다 잘 돌았다. 링이 로드됐다. 요청 하나에 정확히 답했다. 그러고 나서 head 노드가 죽었다.
어댑터의 release 회계에 들어 있는 검사.
let Endpoint::Node { generation, .. } = &event.envelope.source;
if *generation != receipt.load_generation {
return Err("release owner census generation differs from source");
}끝난 요청을 닫는 receipt는 load generation을 담고, 그것을 보내는 노드는 자기 generation을 담는다. 둘이 다르면 그 노드는 정지된다. 그래서 이 버그의 모양은 이렇다: 로드 성공, 첫 요청 성공, head 사망, 그 뒤의 모든 세션이 부분 로드 상태로 멈춤. 부하를 받다 죽은 것처럼 읽히지, 값 불일치처럼은 전혀 읽히지 않는다. 이제 우리 로더는 두 숫자가 어긋나는 계획을 아무것도 로드하기 전에 거부한다.
결코 만족될 수 없는 한계값
어댑터는 스테이지가 반환할 수 있는 최대 결과 크기를, 스테이지 서버가 올라오며 보고하는 값과 정확히 같은지 비교한다. 그 값은 모델을 로드해 보기 전에는 알 수 없었다 — 그래서 추측값으로 로드했고, 실패가 네 스테이지의 실제 숫자를 한꺼번에 알려 줬다:
한 번의 실행, 네 개의 답.
step37-s0: profile=33554432, READY=34419218444 step37-s1: profile=33554432, READY=34419218444 step37-s2: profile=33554432, READY=34419218444 step37-s3: profile=33554432, READY=59136012
34 GB. 에이전트의 retained store는 256 MiB이니 그 링은 애초에 승인될 수 없었다. 스테이지 서버에서 유도식을 읽어 보니 이유가 드러났다: 이 한계값은 n_batch × n_ubatch에 비례하는데, 우리는 이 검사가 생기기 전의 설정에서 배치 폭 2048을 물려받고 있었다. 프로덕션 레이아웃이 실제로 쓰는 폭인 128행에서는 한계값이 138 MB이고 들어맞는다. 이제는 기억해 둔 상수를 들고 다니는 대신 계획 단계에서 같은 공식으로 유도하며, tail 스테이지의 예측값은 다른 배포가 기록해 둔 숫자와 정확히 일치했다 — 20분짜리 로드에 들어가기 전에 확보해 둘 만한 종류의 일치다.
짧게
- 두-호스트 링의 loopback 주소. 스테이지는 다음 스테이지의 에이전트를 그 에이전트가 광고하는 주소로 dial한다.
127.0.0.1을 광고하면 호스트 A는 자기 자신을 dial한다. 이 링의 기록된 설정은 처음부터 loopback이었다 — 호스트를 넘어서는 애초에 동작할 수 없었다. - 저널은 필수다. 에이전트의 운영 저널 없이는 모델이 로드되지 않는다. 우리는 다른 오류를 쫓다가 저널을 껐고, 증상을 진전처럼 보이는 방식으로 더 나쁘게 만들었다.
- 디바이스 이름은 백엔드마다 다르다. 레퍼런스 계획 빌더는 CUDA를 겨냥해
--device CUDA0을 내보낸다. HIP 빌드는 디바이스를ROCm0이라 부른다. 그 계획은 모델 전체를 로드하고 *그다음에* 디바이스를 찾지 못한다. - 네이티브 서버는 릴리스의 일부다. 더 새 트리에서 빌드한 에이전트는 설치된 스테이지 서버가 보고하지 않는 능력을 요구한다. 이것 역시 모델을 전부 로드한 뒤에 발견된다.
비싼 실패 모드는 침묵이다
하나하나는 고치기 싸고 찾기 비쌌으며, 패턴은 일관된다: 비싼 실패는 멀쩡해 보이는 시스템이 아무것도 하지 않거나 딱 한 번만 하던 경우였다. 우리가 추가한 가드는 전부 같은 모양이다 — 실수가 아직 읽히는 자리에서 일찍 거부하기. 로더는 첫 명령이 떠나기 *전에* load generation을 디스크에 쓴다. 그러지 않으면 복구가 불가능하기 때문이다. generation 불일치는 첫 요청 뒤에 발견하는 대신 미리 거부한다. 결과 한계값은 기억하는 대신 유도한다.
링이 서빙 중이다. 머신 두 대에 걸친 네 개 스테이지, 상주 가중치 113 GiB, 첫 토큰은 콜드 1.4초·웜 약 0.3초, 그리고 노드별 기여가 처음으로 정산 원장까지 흘러갔다.