Hub 버킷으로 조 개의 파라미터 배포하기: TRL의 델타 웨이트 동기화
TL;DR, 학습할 모델이 많고 시간을 존중하니까:
- 비동기 RL에는 더러운 비결이 있다: 매 스텝마다 트레이너가 전체 모델을 추론 엔진으로 보내야 한다. 7B 모델을 bf16으로 표현하면 14GB다. 프론티어급 1조 파라미터 모델 체크포인트라면 대략 테라바이트 규모다. 매 스텝마다.
- 그럴 필요가 없다는 게 밝혀졌다. 연속된 두 RL 옵티마이저 스텝 사이에, 대략 99%의 bf16 가중치가 비트 단위로 동일하다(최악의 경우도 98% 이상). 실제 변화는 매우 작다.
- 우리는 변경된 요소만을 희소 safetensors 파일로 인코딩하고 Hugging Face 버킷에 업로드한 후 vLLM에 다운로드하라고 알려주는 TRL PR을 완성했다. Qwen3-0.6B에서 스텝당 페이로드가 1.2GB에서 20~35MB로 떨어진다.
- 게다가: 트레이너가 한 머신에, vLLM이 Hugging Face Space에, Wordle 환경이 다른 Space에 있는 완전히 분산된 학습을 실행했고, 가중치는 단일 Hub 버킷을 통해 흘렀다. 공유 클러스터도, RDMA도, VPN도 없었다.
비동기 RL이 훨씬 저렴해졌다. 계속 읽어보자.
1. 1테라바이트 문제
비동기 RL 학습의 환경에 관한 이전 글을 읽었다면 이미 알고 있을 것이다. 모든 비동기 RL 라이브러리는 "액터 모델"을 어떻게 쓰든, NCCL 백엔드가 무슨 색이든 결국 같은 근본적인 문제에 부딪힌다: 가중치 동기화.
추론 엔진은 스텝 N의 정책으로 말한다. 트레이너는 방금 스텝 N+1을 완료했다. 새 가중치가 한쪽에서 다른 쪽으로 이동해야 추론 엔진이 정책에서 심하게 벗어나기 시작하기 전에. 동기식이든 비동기식이든 관계없이 이것이 임계 경로에 있다: 블로킹 전송은 토큰을 생성하지 않는 GPU의 낭비된 유휴 계산이다. 희소 델타 경로를 사용하면 그 유휴 시간을 초 단위로 줄일 수 있고, 트레이너는 추론 엔진이 준비될 때까지 기다릴 필요도 없다: 옵티마이저 스텝이 끝나자마자 "가중치 준비됨"을 발행하고 공유 버킷에 가중치를 업로드할 수 있고, 추론 엔진은 자신의 속도대로 다운로드한다.
Fireworks는 그들의 글 Frontier RL은 생각보다 저렴하다에서 이에 대해 매우 인상적인 수치를 제시했다: 프론티어급 1조 파라미터 체크포인트를 fp8로(그들의 설정) 표현하면 전체 스냅샷은 1024GB이고, 이것이 롤아웃 플릿을 업데이트할 때마다 배송해야 한다고 기존 지혜가 말한다. 이런 수치면 사람들이 메가 클러스터, RDMA 패브릭, 전용 크로스 리전 링크로 다이어그램을 그리기 시작한다. 그들의 측정된 평균 델타는 인접한 체크포인트 간에 20.3GB, 즉 전체 모델의 1.98%이고, "bf16 형식의 가중치 중 98% 이상이 연속된 체크포인트 간에 비트 동등성을 유지한다".
Cursor의 Composer 2 보고서는 유사한 이야기를 한다. 그들은 학습과 추론을 다른 지역에서 실행하고 공유 S3 버킷(그들의 정확한 말)으로 연결하며, 트레이너가 매 학습 스텝마다 압축된 가중치 델타를 업로드한다. 각 클러스터는 공유 델타 체인으로부터 독립적으로 다운로드하고 재구성하며, "학습 클러스터에 대한 직접 연결이 필요 없다". 두 측은 파라미터에 대해 직접 말한 적이 없다. 버킷이 선이다.
두 논문은 세 가지에 동의하고, 이것들을 천천히 반복하고 싶다. 이 글의 나머지는 본질적으로 충실한 오픈소스 번역이기 때문이다:
- 대부분의 가중치는 실제로 두 인접한 RL 스텝 사이에 변하지 않았다.
- 변경된 부분만 보내면 대역폭 비용이 대략 두 자릿수 만큼 줄어든다.
- 그 작은 델타를 공유 객체 저장소를 통해 라우팅하면 트레이너와 추론 클러스터가 같은 데이터 센터에 있을 필요가 없다.
유일한 빠진 것은 이 이야기를 pip install할 수 있는 버전이었다. 그래서 우리가 만들었다.
2. bf16 RL 가중치가 거의 항상 희소인 이유
무엇이든 연결하기 전에, 이 전체 게임이 왜 승리할 수 있는지 이해할 가치가 있다. "98%의 가중치가 변하지 않는다"는 주장은 데모에서는 작동하고 현장에서 무너진다는 그런 수치 같다. 그렇지 않다. bf16 산술이 RL이 사용하는 학습률에서 어떻게 작동하는지에서 나온다.
bf16 숫자는 7비트 가수를 가진다. 연속된 2의 거듭제곱 사이에 정확히 개의 표현 가능한 값이 있으므로, 근처의 인접한 bf16 숫자 사이의 간격은 대략 다. 업데이트는 일 때 그 간격의 절반보다 아래에 있을 때마다 bf16 캐스트에 흡수된다. 이것이 PULSE가 그들의 그림 3에서 그리는 "bf16 가시성 임계값"이다.
이제 Adam이 하는 것을 보자. RL 학습률이 라고 하면, 단일 가중치에 대한 업데이트는: 이다.
정규화된 스텝 는 대략 1 정도이므로, 이다. 대부분의 가중치에 대해, 는 대략 에서 사이에 있다(PULSE는 대표적인 LLM 가중치에 대해 중앙값 0.019를 보고한다). 임계값 는 그 규모에서 대략 에서 사이이고, 이것은 업데이트보다 크다.
다시 말해: 옵티마이저가 속삭이고 있고 bf16이 듣지 못한다. 업데이트는 반올림에 의해 흡수되고, 의 바이트 표현은 변하지 않고, 추론 엔진의 관점에서 이 가중치는 움직이지 않았다. 이것을 몇 억 개의 파라미터로 곱하면 >99% 희소성 수치를 얻는다, 공짜로, 근사 없이.
이것은 정확히 PULSE 논문(Mihai & Belilovsky, 2026)에서 공식화된 주장이다. 그들은 두 개의 임계값을 정의한다. 흡수 경계 는 Adam 업데이트의 보수적인 최악의 경우이고, 효과적 경계 는 실제로 살고 있는 체제다. bf16 가시성 임계값은 이다. 업데이트가 가시성 임계값 아래에 있을 때마다 흡수되고 bf16 바이트는 변하지 않는다. 그들의 그림 3은 대표적인 LLM 가중치의 구름에 대해 두 경계를 그리고, 결론은 명확하다: 에서, 흡수 경계 자체는 이미 모델의 거의 모든 가중치에 대해 가시성 임계값 아래에 있다. 그들은 Qwen2.5(0.5B/1.5B/7B), Llama-3.2-3B, Gemma-3-4B에 걸쳐 경험적으로 측정하고 400 학습 스텝 동안 표준 편차가 0.2~0.4%인 ~99%의 일정한 스텝별 희소성을 일관되게 찾는다. 최악의 경우 스텝은 98% 이상을 유지한다. 따라서 <1%가 변경되었다는 것은 운이 좋은 측정이 아니다; 이것이 산술이 보장하는 것이다.
우리는 이것을 분석적으로 예측할 필요가 없다(그리고 실제로 우리는 Adam의 과 통계로부터 변경 마스크를 예측하려고 시도했지만 재현율이 슬픈 30%였다, 나중에 더 자세히). 우리는 단지 어느 바이트가 뒤집혔는지 관찰할 필요가 있다. 이것은 파라미터당 매우 작은 부울 텐서이며, 옵티마이저 스텝 근처에서 계산된다.
3. HF 버킷과 아키텍처
여기가 이야기의 두 번째 부분이 들어오는 곳이고, 이 글이 Fireworks/Cursor의 번역을 멈추고 Hugging Face 것이 되는 곳이다.
3.1 버킷이란?
버킷은 고주파 객체 저장소를 위해 설계된 Hub의 리포 유형이다. 커밋 의식도, PR 워크플로도, LFS의 이상함도 없다. 파일을 추가하고, 파일을 나열하고, 파일을 다운로드한다. Python 인터페이스는 두 함수다:
from huggingface_hub import batch_bucket_files, download_bucket_files
batch_bucket_files("my-org/wordle-deltas", add=[(buffer, "deltas/step_000042.safetensors")])
download_bucket_files("my-org/wordle-deltas", files=[("deltas/step_000042.safetensors", local_path)])
이게 다다. 두 함수 호출로 가중치가 비행 중이다.
후드 아래에서 버킷은 Xet, Hub의 콘텐츠 정의 청킹 저장소 계층으로 지원된다. Xet는 업로드하는 모든 파일을 보고, 고정 오프셋이 아닌 실제 콘텐츠를 기반으로 청크로 자르고, 버킷에 이미 있는 모든 것에 대해 중복을 제거한다. 실질적인 결과는 이 문맥에서 기쁜데, 우리가 희소 인코딩을 쓰기에 너무 게으르고 매 스텝 전체 앵커를 업로드했더라도 Xet가 여전히 변경된 청크만 전송할 것이다. 희소 인코딩 + Xet 스택: 우리는 이동한 것에 대해 비용을 지불하고 한 번 비용을 지불한다.
이것은 Fireworks와 Cursor가 모두 도달하는 "공유 S3 버킷"의 오픈소스 동등물이지만, 저장소 계층이 이미 콘텐츠 해싱을 알고 있고, 기존 HF 토큰이 이미 허가를 가지고 있으며, 스택의 나머지와 자연스럽게 작성된다(Spaces, 데이터셋, 모델).
3.2 세 개의 상자
전체 아키텍처는 정확히 세 개의 상자와 하나의 공유 기판을 가지고 있다:
- 트레이너. 원하는 곳 어디든. 하나의 GPU, 8개의 GPU, USB 연결 H100이 있는 노트북, 우리는 판단하지 않을 것이다. 모델 가중치를 소유하고, 옵티마이저를 실행하며, 희소 델타를 방출한다.
- HF 버킷. 단일 리포, 두 개의 접두사: 가끔 전체 스냅샷을 위한
anchors/와 그 사이의 희소 패치를 위한deltas/. 이것이 유일한 것으로 양쪽이 동의한다. - vLLM 롤아웃 서버. 원하는 곳 어디든, 그리고 매우 중요하게는 반드시 트레이너가 있는 곳에 있을 필요는 없다. 버킷에서 끌어오고, 델타를 적용하며, 롤아웃을 제공한다.
- 환경. 일반적인 방식으로 롤아웃 서버를 매달고(HTTP, 함수 호출, 무엇이든 env가 말하는 것).
내재화할 속성, Cursor의 논문이 열심히 판매하는 것이고 여기서 정확하게 보유한다: 트레이너와 롤아웃 서버는 가중치에 대해 절대 말하지 않는다. 그들은 {"repo_id": ..., "filename": ...}를 포함하는 작은 POST를 교환하고, 그것이 전체 제어 평면이다. 실제 바이트 전송은 각 측과 버킷 사이에서 병렬로 공유 네트워크 패브릭 없이 발생한다.
그것이 실제로 중요한 이유:
- 롤아웃 서버는 다른 리전, 다른 클라우드, 또는 Hugging Face Space 내에서 NAT 뒤에 있을 수 있다. 신경 쓰지 않는다.
- N 추론 복제본이 같은 버킷에서 같은 델타를 당길 수 있고, Xet는 모두에 걸쳐 바이트를 중복 제거한다.
- 트레이너는 존재하거나, 어디에 있거나, 그 중 하나가 방금 충돌했는지 추론 복제본 수를 알 필요가 없다.
트레이너가 쓴다. 복제본이 읽는다. Hub가 배관을 한다.
4. 프로토콜
이제 후드를 열 수 있다. 프로토콜은 네 부분을 가진다: 와이어 포맷, 버킷 레이아웃, 30줄 vLLM 확장, 트레이너 측 변경 검출기. 듣는 것보다 실제로 더 적은 코드다.
4.1 와이어 포맷으로 Safetensors
우리는 온디스크 및 온와이어 포맷을 위해 safetensors를 선택했다. 이미 Hub의 정규 체크포인트 포맷이고, 모든 합리적인 프레임워크가 읽을 수 있으며, 헤더가 임의 문자열 메타데이터를 가진다. 그 메타데이터 필드가 프로토콜을 숨기는 곳이다.
버킷에는 두 종류의 파일이 있다.
앵커는 정상적인 체크포인트처럼 보인다: 파라미터당 하나의 텐서, 전체 bf16 가중치, 매 동기마다 작성된다(기본값은 ).
anchors/step_000010.safetensors
├── model.layers.0.self_attn.q_proj.weight (bf16, full)
├── model.layers.0.self_attn.k_proj.weight (bf16, full)
└── ...
metadata:
sparse=False, model_version=10, sparsity=0.0
델타는 흥미로운 부분이다. 실제로 변경된 각 파라미터에 대해 두 개의 항목을 저장한다: 요소 인덱스의 평면 int32 텐서와 해당 인덱스에서의 bf16 텐서 값.
deltas/step_000011.safetensors
├── model.layers.0.self_attn.q_proj.weight.indices (int32, [num_changed])
├── model.layers.0.self_attn.q_proj.weight.values (bf16, [num_changed])
├── model.layers.0.mlp.gate_proj.weight.indices
├── model.layers.0.mlp.gate_proj.weight.values
└── ...
metadata:
sparse=True, model_version=11, sparsity=0.9938, changed_params=[...]
이 선택의 좋은 결과들이 몇 가지:
- 델타는 파일이다. Python에서
safe_open(...)로 열 수 있고 그 안의 모든 텐서를 검사할 수 있다. 독점적인 프레이밍, 길이 접두사, 버전 악수 없음. - 메타데이터는 자기 설명적이다. 수신자가
sparse=True/False를 읽고 분기한다. 별도 매니페스트가 없다. - 추론 측에서 mmap을 통해 제로 복사인데, 이것이 중요한데 수 초마다 이것을 할 때이기 때문이다.
케이던스는 간단하다: 매 N번째 스텝에서 앵커, 그 사이에 델타. 둘 다 anchors/와 deltas/ 접두사 아래의 같은 버킷에 끝난다. 각 새로운 추론 복제본은 가장 최근의 앵커를 잡은 후 그 이후의 델타를 재생하기만 하면 된다.
4.2 트레이너 측: 옵티마이저 후크로부터의 부울 마스크
트레이너는 실제로 어떤 bf16 요소가 뒤집혔는지 알아야 한다. 우리는 옵티마이저에 사전 스텝 및 스텝 후 후크를 등록하는 작은 BF16ChangeDetector로 이것을 한다:
class BF16ChangeDetector:
def __init__(self, model, optimizer):
self._pre_step_bf16: dict[str, torch.Tensor] = {}
self._validated_masks: dict[str, torch.Tensor] = {}
optimizer.register_step_pre_hook(self._pre_step_hook)
optimizer.register_step_post_hook(self._post_step_hook)
def _pre_step_hook(self, opt, args, kwargs):
for p in self._params:
self._pre_step_bf16[name_of(p)] = p.detach().to(torch.bfloat16).cpu().clone()
def _post_step_hook(self, opt, args, kwargs):
for p in self._params:
self._validated_masks[name_of(p)] = (
p.detach().to(torch.bfloat16).cpu() != self._pre_step_bf16[name_of(p)]
)
PR의 실제 코드는 약간 더 많은 배관을 가진다(Accelerate가 다른 Python 객체로 래핑하기 때문에 data_ptr()를 통해 옵티마이저 파라미터 객체를 모델 파라미터와 매칭), 하지만 아이디어는 냅킨에 들어간다: 스냅샷, 스텝, 차이.
이것이 사실의 근거다. 우리는 Adam의 과 통계에서 마스크를 예측하는 더 우아한 경로를 시도했다, bf16 ULP 임계값을 직접 사용한다. 원칙적으로는 작동한다. 실제로는 재현율이 약 30%였는데, 이는 우리가 실제 업데이트의 3분의 2를 놓친 델타를 배송했을 것임을 의미한다. Adam의 정규화는 혼란스러워서 분석 임계값이 타이트하지 않다. 그래서 우리는 단지 바이트를 비교한다. 트레이너 측에서 모델의 하나의 bf16 CPU 스냅샷 비용이 든다, 우리는 지불할 의사가 있다.
새로운 _sync_weight 흐름의 네 단계는:
- 추론이 계속 실행되는 동안 업로드한다. 트레이너가 마스크된 요소를 safetensors 버퍼로 인코딩하고 버킷으로 푼다. vLLM은 이 전체 스텝 동안 여전히 행복하게 구 정책을 제공하고 있다.
- vLLM을 일시 중지한다. 짧은 HTTP 호출, 수백 밀리초.
/update_weights를 신호한다. 버킷 좌표를 보낸다. vLLM이 다운로드하고, 적용하고, 반환한다.- 다시 시작한다. vLLM이 다시 공중에서.
로그 라인이 이야기를 말한다:
Delta: 1234567/200000000 elements changed (sparsity=99.38%)
[delta_engine] uploaded user/wordle-deltas/deltas/step_000042.safetensors (27.4 MB, ...)
Weight sync: done. Total 9.4s (inference paused 1.1s)
중요한 라인은 괄호다. 추론이 1.1초 일시 중지되었다. 남은 9.4초는 업로드에 소비되었고, 이는 롤아웃 서버가 여전히 토큰을 생성하는 동안 발생했다. NCCL의 경우 우리는 전체 동기 시간을 일시 중지 시간으로 지불했다. 여기서 우리는 배경 시간으로 지불한다.
4.3 vLLM 측: 30줄 확장
vLLM은 WeightTransferEngine이라고 불리는 이것을 위한 깨끗한 추상화를 가진다. 우리는 receive_weights 메서드가 정신으로:
def receive_weights(self, update_info, load_weights):
download_bucket_files(update_info.repo_id, files=[(update_info.filename, local_path)])
with safe_open(local_path, framework="pt", device="cpu") as f:
meta = PatchMetadata.from_metadata_dict(f.metadata())
if not meta.sparse:
for name in f.keys():
tensor = f.get_tensor(name)
self._bf16_snapshot[name] = tensor.clone()
load_weights([(name, tensor)])
else:
for name in json.loads(meta.changed_params):
indices = f.get_tensor(f"{name}.indices").long()
values = f.get_tensor(f"{name}.values")
snap = self._bf16_snapshot[name].flatten()
snap[indices] = values
self._bf16_snapshot[name] = snap.reshape(self._bf16_snapshot[name].shape)
load_weights([(name, self._bf16_snapshot[name])])
우리는 vLLM의 --worker-extension-cls 플래그를 통해 등록하는데, 이는 vLLM의 포크가 필요 없다는 의미다. vLLM과 같은 이미지에 TRL을 설치하고, 우리 클래스에 CLI를 가리키고, 완료다.
언급할 가치가 있다: vLLM 자체는 희소 가중치 전송을 기본적으로 착지시키는 진행 중인 노력이 있다, vllm-project/vllm#40096. 이것은 receive_sparse_weights()와 trainer_send_sparse_weights()를 WeightTransferEngine 베이스 클래스에 직접 추가하고, 패치를 (indices, values)로 인코딩하고 index_copy_()를 통해 제자리에 적용하며, GPU/CPU 검증 왕복을 전적으로 제거한다. PR은 Qwen3-1.7B에서 희소 패치의 0.16MB를 0.40ms에 대해 전체 조밀 전송의 942MB를 192ms에 보고한다.
추론 측의 우리 구현의 한 가지 정직한 주의: 우리는 CPU bf16 스냅샷을 모델의 유지하므로 우리는 희소 (indices, values) 패치로부터 전체 텐서를 재구성할 수 있는데, vLLM에서 load_weights가 오늘 전체 텐서를 기대하기 때문이다. #40096 (또는 그 후속물)이 착지하고 제자리 희소 load_weights 경로를 드러내면, 우리는 GPU에 지수를 직접 적용할 수 있고 스냅샷을 떨어뜨린다!
5. Spaces에 실제로 세우기
이것이 자랑스러워하는 부분이다. 지금까지 우리가 설명한 모든 것이 노트북에서 작동하지만, 가중치를 Hub 버킷을 통해 라우팅하는 이유는 트레이너와 롤아웃 서버가 서로 가까이 있을 필요가 없다는 것이다. 그래서 우리는 네트워크를 공유하지 않는 세 머신으로 완전히 분산된 학습을 실행했다:
- 하나의 GPU로 트레이너를 실행하는 상자.
- Hugging Face Space(Docker SDK, L4 GPU)는 우리의 확장 클래스와 함께 vLLM을 실행한다.
- 두 번째 Hugging Face Space(CPU)는 256개의 동시 세션 용량으로 Wordle 환경 서버를 실행한다.
- 중간에 Hub 버킷.
이것을 세우는 것은 정말 몇 가지 hf CLI 호출이다. vLLM Space의 Dockerfile은 본질적으로 업스트림 vLLM 이미지 더하기 pip install trl@... 더하기 진입점:
FROM vllm/vllm-openai:latest
RUN pip install "trl @ git+https://github.com/huggingface/trl.git@delta-weight-sync"
ENV VLLM_SERVER_DEV_MODE=1
EXPOSE 7860
ENTRYPOINT ["vllm", "serve", "Qwen/Qwen3-1.7B", \
"--host", "0.0.0.0", "--port", "7860", \
"--worker-extension-cls", "trl.experimental.async_grpo.delta_engine.DeltaWorkerExtension", \
"--weight-transfer-config", "{\"backend\":\"nccl\"}", \
"--max-model-len", "32768", \
"--gpu-memory-utilization", "0.8"]
Space로 배포한다:
hf repos create $USER/vllm-wordle-inference \
--type space --space-sdk docker --flavor l4x1 \
--secrets HF_TOKEN=$HF_TOKEN
hf upload $USER/vllm-wordle-inference examples/scripts/openenv/vllm_space/ --type space
그리고 지구의 어느 곳에서든 HTTPS로 말할 수 있는 곳에서 학습을 시작한다:
python examples/scripts/openenv/async_wordle.py \
--vllm-server-url https://$USER-vllm-wordle-inference.hf.space \
--env-url https://openenv-wordle.hf.space \
--delta-sync-repo-id $USER/wordle-deltas \
--model Qwen/Qwen3-1.7B
트레이너는 포트를 열지 않는다. Space는 트레이너의 IP를 보지 못한다. Wordle 환경은 둘 다 존재하는지 모른다. 그들은 모두 Hub로 말한다. 학습은 즉시-EOS 제안 확인에서 수렴했고, 그 다음 실제 Wordle 롤아웃: 보상이 올라가고, 델타 페이로드는 20~35MB 밴드에 머물렀고, 동기당 추론 일시 중지 창은 약 1초로 유지되었다. 전체 실행 로그는 동반 PR에 링크된다.
6. 그래서 이것이 실제로 무엇을 잠금 해제하는가?
몇 가지이고, 우리는 그들이 크다고 생각한다.
클러스터 없는 비동기 RL 학습. 하나의 GPU와 Hugging Face 계정이 있으면 이제 진짜 분산된 학습을 할 수 있다. 트레이너는 GPU에 있고; 롤아웃 플릿은 Spaces에 산다; 환경은 다른 Space에 산다; 가중치는 버킷을 통해 이동한다. 이것은 공동 위치 설정(그것이 가져오는 모든 처리량 손실)이나 공유 네트워킹이 있는 실제 클러스터를 필요로 했다. 더 이상 아니다.
다중 복제본 추론, 공짜로. 두 개의 vLLM Space를 세우거나, 10개. 그들은 모두 같은 버킷에서 끌어온다. Xet 콘텐츠 주소 저장소가 연속된 앵커가 쉬고 있는 청크를 공유하고(버킷이 폭발하지 않도록 유지), Hub의 모서리 캐시가 같은 파일의 반복 다운로드를 배치하기 싸게 만든다. 전 지구적으로 분산된 롤아웃 플릿을 원하는가? 이제 작은 DevOps 연습이다, 연구 프로젝트 아니다.
기존 도구로 디버깅할 수 있는 와이어 포맷. 델타는 safetensors 파일이다. 노트북에서 safe_open할 수 있고, 키를 나열하고, 지수를 검사하고, 희소성을 자신이 계산할 수 있다. 우리는 불투명한 NCCL 스트림에서 충분한 시간을 tcpdump 화했으므로 이것을 감사한다.
프론티어 규모로의 경로. 20~35MB 수치는 Qwen3-0.6B를 위한 것이다. 흥미로운 질문은 한 번 다이얼을 올린 후 곡선이 어떻게 보이는지 무엇인가. 냅킨 수학을 하자.
Llama-3.1-405B를 가져라. bf16에서 이것은 디스크에 810GB다. PULSE는 RL 학습률에서 ~99% 평균 스텝별 희소성을 측정하므로 실제 델타는 파라미터의 약 1%에 앉는다. 그들의 배포 측정 인코딩은 7B 모델에서 108MB를 명중하는데, 이것이 PULSE가 보고하는 ~130× 감소다. 405B로 선형으로 확장하면, 델타는 스텝당 대략 6GB에 착지한다.
그것이 벽시계에서 무엇을 사는지? NCCL은 클러스터 내에서 빠르다, 확실히. 관대한 100GB/s 집합 브로드캐스트 대역폭을 가정한다(다중 노드, RDMA, 전부). 전체 동기는 810GB / 100GB/s ≈ 8초 추론 일시 중지, 매 스텝. 델타 경로에서, 트레이너가 생성이 계속하는 동안 배경에서 버킷에 6GB를 흐르게 하고, 롤아웃 서버의 실제 일시 중지 창은 단지 적용 스텝이고, 이 규모에서는 수 초 정도에 착지한다. 그래서 우리가 클러스터를 떠나기도 전에, 델타는 보이는 일시 중지를 4× 줄이고 와이어의 바이트를 ~130×.
이제 클러스터를 떠난다. NCCL은 클라우드를 가로질러 직선적으로 작동하지 않는다. 일단 us-east에 롤아웃 플릿을 원하면, 다른 하나는 eu-west에, 아마도 Hugging Face Space에 하나, 버킷 기반 경로가 유일한 경로다. 1GB/s의 사용 가능한 인터넷 대역폭에서, 단일 전체 브로드캐스트는 13분이 걸릴 것이다; 델타는 6초에 한다.
Fireworks 프레이밍에서 1TB급 모델의 경우, 그들 자신의 측정된 수치는 20.3GiB 델타 vs 1024GiB 전체 스냅샷, 약 50× 감소를 보여준다. PULSE의 더 타이트한, 희소 인코딩은 그것을 더 멀리 밀 것이다(외삽 ~15GB 스텝당 델타, ~65×에 더 가깝다). 어쨌든, 너는 상품 객체 저장소를 통해 가중치를 배송하는 것이 해킹에서 유일한 합리적인 아키텍처로 시작하는 체제에 있다.
7. 아직 우리 판 위에 있는 것
우리는 이것이 끝났다고 가장하지 않는다. 정직한 목록이다.
- 두 개의 CPU bf16 스냅샷, 하나 너무 많다. 트레이너는 하나를 유지한다(변경 검출기를 위해) 그리고 롤아웃 서버는 하나를 유지한다(vLLM의
load_weights를 위해 전체 텐서를 재구성하기). 첫 번째는 누군가가 타이트 분석 마스크를 찾을 때까지 우리가 붙어 있는데, 보이는 것보다 더 어렵다. 두 번째는 vLLM이 희소load_weightsAPI를 얻을 때 사라진다. PR이 나올 예정이다. - 고정된 앵커 케이던스. 우리는 현재 매 스텝마다 전체 앵커를 덤프한다. 적응적 정책("누적 드리프트가 X를 초과할 때 앵커")은 긴 실행의 앵커 비용을 줄일 것이다.
- 다중 노드 FSDP2 트레이너.
BF16ChangeDetector는 프로세스당 옵티마이저 후크 주위에 구축된다. FSDP2로 깨끗하게 일반화되어야 하지만 우리는 다중 노드 규모에서 측정하지 않았다. PR의 우리 이름과 함께TODO가 있다. - 옵티마이저로 후킹하기. 우리의 마스크를 단독에서 예측하려는 시도는 낮은 재현율을 주었는데, 분석적 bf16 임계값이 교과서 공식보다 더 미묘한 것을 하고 있음을 의미한다. 이것을 깬 누구에게서든 들어보고 싶다.
- 온 와이어 압축으로 적층하기. 희소 safetensors과 청크당 gzip은 직교한다. 우리는 아직 그들을 결합하려고 시도하지 않았다. 비록 우리는 거대한 압축 이득을 기대하지 않지만.