가중치 전송

가중치 전송 (Weight Transfer)

RLHF, GRPO 같은 온라인 RL 기법에서는 훈련 중 정책 모델이 반복적으로 갱신되고, 그 갱신된 가중치가 롤아웃 생성용 추론 엔진에 반영되어야 해요. 문제는 "훈련 프로세스의 가중치를 어떻게 추론 엔진까지 안전하게 전달할까"라는 거죠. 이 문서는 vLLM이 그 동기화를 처리하는 플러그형 가중치 전송(weight transfer) 시스템을 설명해요.

출처: vLLM 공식 문서 — weight_transfer

아키텍처 (Architecture)

가중치 전송은 프로세스당 두 개의 엔진을 가지며 서로 대칭이에요.

트레이너 프로세스 추론 워커
클래스 TrainerWeightTransferEngine WeightTransferEngine
생성 WeightTransferTrainerFactory.trainer_init(...) vLLM이 WeightTransferConfig에서 생성
구동 send_weights() 아래의 4단계 프로토콜
보유 communicator, 전송 계획, wire params communicator, 대상 모델

트레이너 엔진은 상태를 가져요. 자신의 communicator와 wire params를 소유하고, WeightSource에서 가중치를 끌어오며, VLLMWeightSyncClient를 통해 추론 쪽을 구동합니다. 트레이너 코드는 전송 방식을 알 필요가 없고, 매 라운드마다 전송 상태를 다시 넘길 필요도 없어요. 동기화당 send_weights() 호출 하나만으로 충분하죠.

내부적으로 모든 라운드는 같은 4단계 프로토콜이며, 트레이너 엔진이 대신 구동해줘요.

  1. 초기화 (init_weight_transfer_engine): 트레이너와 추론 워커 사이의 통신 채널을 구축. 훈련 루프가 시작되기 전에 trainer_init에서 한 번 호출.
  2. 시작 (start_weight_update): 가중치 갱신을 위해 추론 엔진을 준비.
  3. 가중치 갱신 (update_weights): 갱신된 가중치를 전송. 한 번 이상 호출될 수 있어요(예: 청크 전송).
  4. 종료 (finish_weight_update): 갱신을 마무리(예: 체크포인트 형식 가중치의 사후 처리). 모든 가중치가 전송된 뒤 한 번 호출.

사용 가능한 백엔드 (Available Backends)

백엔드 전송 방식 사용 사례
NCCL NCCL broadcast 훈련과 추론이 별도 GPU
IPC CUDA IPC handles 같은 GPU에서 훈련과 추론 컬로케이트
sparse_nccl NCCL broadcast 체크포인트 좌표 기반 희소 가중치 패치
sharded_rdt NIXL / Ray Direct Transport (pull 기반) 각 워커가 자기 슬라이스만 필요한 매우 큰 모델 (전문가 병렬을 쓰는 MoE)

퀵스타트 (Quickstart)

추론 쪽 (Inference Side)

추론 쪽은 백엔드 이름만 받아요. 전송에 관한 나머지는 트레이너가 정해서 초기화 핸드셰이크에서 보내줍니다.

from vllm import LLM
from vllm.config import WeightTransferConfig

llm = LLM(
    model="my-model",
    weight_transfer_config=WeightTransferConfig(backend="nccl"),  # or "ipc", "sparse_nccl", "sharded_rdt"
)

온라인 서빙이라면 다음과 같이요.

vllm serve my-model \
    --weight-transfer-config '{"backend": "nccl"}'

트레이너 쪽 (Trainer Side)

엔진을 한 번 만들고, 매 동기화마다 send_weights()를 호출하면 돼요.

from vllm.distributed.weight_transfer import (
    ModuleSource,
    HTTPVLLMWeightSyncClient,
    WeightTransferTrainerFactory,
)
from vllm.distributed.weight_transfer.nccl_engine import NCCLTrainerInitInfo

# Once, before the training loop.
engine = WeightTransferTrainerFactory.trainer_init(
    init_info=NCCLTrainerInitInfo(
        master_address=master_address,
        master_port=master_port,
        world_size=world_size,   # trainer + all inference workers
        rank=0,                  # this trainer rank; rank 0 is the sender
        packed=True,
    ),
    client=HTTPVLLMWeightSyncClient("http://localhost:8000"),
    source=ModuleSource(model),
)

# Once per weight sync.
for step in range(num_steps):
    train_one_step(model)
    engine.send_weights()

send_weights()는 추론 쪽의 start → update → finish 그리고 데이터 플레인 전송을 구동해요. 여기에는 백엔드가 필요로 하는 동시성도 포함되죠(NCCL은 예를 들어 워커의 update_weights와 트레이너의 broadcast를 동시에 실행해야 합니다. 둘 다 같은 NCCL 호출 안에서 rendezvous 하니까요).

트레이너 쪽에는 backend= 인자가 없어요. 각 TrainerInitInfo 서브클래스가 자신의 backend를 선언하고, 팩토리가 그걸로 디스패치합니다.

각 설정이 사는 곳 (Where Each Setting Lives)

트레이너는 양쪽이 동의해야 하는 모든 것의 단일 진실 소스(single source of truth)예요. 이것이 이 시스템에서 기억해야 할 핵심 규칙입니다.

설정 사는 곳 비고
백엔드, 추론 쪽 WeightTransferConfig(backend=...) 단순 문자열 선택자 — 이 config가 담는 건 이것뿐
백엔드, 트레이너 쪽 init_info.backend 각 init info의 ClassVar; 손으로 넘기지 않음
wire params (packed, 버퍼 크기) 백엔드의 TrainerInitInfo 트레이너가 trainer_init 중 워커로 전파하므로 양쪽이 어긋날 수 없음
보낼 가중치 WeightSource ModuleSource(model)가 일반 모듈과 FSDP 샤딩 모듈을 모두 다룸
라운드별 페이로드 백엔드의 update info 엔진이 (source.metadata() 또는 델타 백엔드의 라운드별 페이로드에서) 생성; 직접 만들지 않음

참고: wire params는 의도적으로 WeightTransferConfig에도, 라운드별 update info에도 두지 않아요. 트레이너가 초기화 때 보내고 워커가 전달받은 값을 읽기 때문에, 양쪽의 packed 플래그 불일치는 단순히 권장되지 않는 게 아니라 표현 불가능합니다.

멀티 랭크 트레이너 (Multi-Rank Trainers)

샤딩된 트레이너(FSDP, TP/PP/EP)에서는 모든 트레이너 랭크가 엔진을 만들고 send_weights()를 호출해요. 랭크 0이 송신자(sender)이고, communicator를 보유하고 클라이언트와 통화하며 바이트를 와이어에 올리는 건 랭크 0뿐입니다. 송신자가 아닌 랭크도 여전히 WeightSource를 순회하는데, 파라미터를 실체화(materialize)하는 것이 보통 집단 연산(collective)이기 때문이에요(FSDP의 full_tensor() all-gather, Megatron export 등). 일부 랭크가 건너뛰면 교착(deadlock)이 일어날 수 있죠.

각 프로세스의 rank를 init info에 직접 넘기세요. 전역 프로세스 그룹에서 읽지 않는데, FSDP / TP / PP / EP처럼 여러 그룹이 존재하면 모호해지기 때문이에요.

engine = WeightTransferTrainerFactory.trainer_init(
    init_info=NCCLTrainerInitInfo(..., rank=torch.distributed.get_rank()),
    client=client,
    source=ModuleSource(model),
)
engine.send_weights()  # called on every rank

API 엔드포인트 (API Endpoints)

vLLM을 HTTP 서버로 실행할 때 가중치 전송을 위한 다음 엔드포인트를 사용할 수 있어요. HTTPVLLMWeightSyncClient가 처음 네 개를 대신 호출해줍니다.

엔드포인트 메서드 설명
/init_weight_transfer_engine POST 백엔드별 정보로 가중치 전송 엔진 초기화
/start_weight_update POST 가중치 갱신 시작
/update_weights POST 백엔드별 메타데이터로 가중치 배치 전송
/finish_weight_update POST 갱신 종료 및 선택적으로 weight_version 커밋
/update_weight_version POST 모델 가중치 변경 없이 weight_version 갱신
/weight_info GET 최신 커밋된 weight version 조회
/pause POST 가중치 동기화 전 inflight 요청 처리를 위해 생성 일시정지
/resume POST 가중치 동기화 후 생성 재개
/get_world_size GET 추론 워커 수 조회 (NCCL world size 계산에 유용)

참고: HTTP 가중치 전송 엔드포인트는 VLLM_SERVER_DEV_MODE=1이 설정되어야 해요.

Rust 프론트엔드의 선택적 gRPC Control 서비스는 신뢰할 수 있는 sidecar를 위해 동일한 pause, sleep, weight-transfer, weight-version 생애주기를 노출합니다. ServerInfo.rl_capabilities 응답은 weight transfer와 sleep mode가 구성되었는지 보고해요. 백엔드별 init_infoupdate_info는 JSON 메타데이터로 남고, 모델 텐서는 계속 구성된 NCCL, IPC, sparse-NCCL 또는 sharded-RDT 전송을 통해 이동합니다.

시스템 확장 (Extending the System)

시스템의 모든 조각을 교체할 수 있어요. 보내는 가중치(WeightSource), 컨트롤 플레인 전송(VLLMWeightSyncClient), 그리고 전송 자체(각자 팩토리 레지스트리를 가진 두 엔진 ABC)까지요. 자세한 내용은 Base Classes 문서를 참고하세요.

더 알아보기 (Learn more)