IPC 엔진
IPC 엔진 (IPC Engine)
트레이너와 추론을 같은 GPU에 함께 두고 돌린다면, 가중치를 "복사"조차 하지 않고 GPU 메모리를 직접 공유할 수 있다면 얼마나 좋을까요? IPC 엔진은 바로 그 방법이에요. CUDA IPC 핸들을 이용해 같은 GPU 위의 트레이너와 추론 워커가 데이터 복사 없이 GPU 메모리를 직접 공유합니다.
IPC를 언제 쓸까 (When to Use IPC)
- 훈련과 추론이 **같은 GPU(들)**을 공유 (컬로케이트, colocated)
어떻게 동작하는가 (How It Works)
- 트레이너가 각 가중치에 대한 CUDA 텐서를 만들고
torch.multiprocessing.reductions.reduce_tensor로 IPC 핸들을 생성해요. 멀티 GPU 설정(예: FSDP)에서는 각 트레이너 랭크가 자기 GPU에 각 파라미터의 전체 텐서를 실체화한 뒤 핸들을 만들고,ModuleSource가 이를 대신 처리해줍니다. - 모든 트레이너 랭크가 자기 핸들을 all-gather에 기여하고, 송신자가 이를 병합해 각 페이로드가 모든 GPU UUID를 자기 인자에 매핑하게 합니다. 그다음 병합된 핸들을 클라이언트를 통해 추론 엔진으로 보내죠. 각 워커는 자기 GPU의 핸들만 읽습니다.
- 추론 워커는
rebuild_cuda_tensor로 핸들에서 텐서를 재구성해, 트레이너의 GPU 메모리에서 직접 읽어요.
NCCL과 달리 IPC 전송은 직선적이에요. update_weights 자체가 전송이고 클라이언트를 타고 가므로, 겹쳐야 할 동시 broadcast가 없습니다.
참고: 2단계의 핸들 all-gather는 기본 프로세스 그룹에서 실행되므로, 그 그룹이 정확히 컬로케이트된 트레이너 랭크들의 집합이고 송신자가 그 멤버라고 가정해요. 분산 그룹이 없으면 no-op입니다.
경고: IPC 핸들은 직렬화된 Python 객체를 보내는 것을 포함해요. HTTP 전송을 쓸 때는 서버와 클라이언트 양쪽에서 VLLM_ALLOW_INSECURE_SERIALIZATION=1을 설정해야 합니다. IPC 핸들은 HTTP 전송을 위해 pickled되고 base64로 인코딩되기 때문이에요.
추론 쪽 (Inference Side)
from vllm import LLM
from vllm.config import WeightTransferConfig
llm = LLM(model="my-model", weight_transfer_config=WeightTransferConfig(backend="ipc"))
vllm serve my-model --weight-transfer-config '{"backend": "ipc"}'
IPC는 데이터 플레인 rendezvous가 필요 없어서, init_transfer_engine은 채널을 열지 않아요. 핸드셰이크에서 트레이너가 보내는 packed 플래그만 기록하고, receive_weights가 그걸 읽습니다. 그래서 전송이 packed인지 여부는 추론 쪽에서 설정할 일이 절대 없어요.
트레이너 쪽 (Trainer Side)
from vllm.distributed.weight_transfer import (
ModuleSource,
HTTPVLLMWeightSyncClient,
WeightTransferTrainerFactory,
)
from vllm.distributed.weight_transfer.ipc_engine import IPCTrainerInitInfo
engine = WeightTransferTrainerFactory.trainer_init(
init_info=IPCTrainerInitInfo(rank=0, packed=False), # rank 0 is the sender
client=HTTPVLLMWeightSyncClient("http://localhost:8000"),
source=ModuleSource(model),
)
engine.send_weights() # once per sync
send_weights()는 start_weight_update, 전송 자체, 그리고 finish_weight_update를 구동하고, 포스트 전송 barrier까지 IPC 공유 복사본에 대한 강한 참조를 유지해요. 그렇지 않으면 소비자의 뷰가 매달릴 수 있기 때문이죠.
어떤 VLLMWeightSyncClient든 여기서 동작해요. 내장 HTTP, Ray 클라이언트, 또는 자체 스택용 어댑터 모두 말이죠. 엔진은 어느 쪽이든 동일합니다.
IPCTrainerInitInfo
| 필드 | 기본값 | 설명 |
|---|---|---|
rank |
— | 키워드 전용. 이 트레이너 프로세스의 랭크; 0이 송신자 |
packed |
False |
청크형, 메모리 제한 전송 (아래 참고) |
packed_buffer_size_bytes |
1 GiB | packed=True일 때의 청크 크기 |
packed는 반드시 동의해야 하는 wire param이에요. trainer_init이 워커로 보내고, 워커가 기록한 뒤 그에 따라 디코딩합니다. WeightTransferConfig 필드도, 라운드별 update_weights 필드도 아니에요. packed_buffer_size_bytes는 생산자만 사용하고, 소비자는 IPC 핸들 + 청크별 tensor_sizes로 재구성하므로 버퍼 크기가 필요 없습니다.
Packed (청크형) 전송
기본적으로 모든 가중치는 단일 update_weights 호출로 보내져요. 대형 모델에서는 이것이 양쪽 모두에 전체 모델이 동시에 GPU 메모리에 있어야 한다는 뜻이죠. packed=True로 설정하면 메모리 제한이 있는 청크 전송이 가능해집니다.
- 가중치가 고정 크기 packed 버퍼(
packed_buffer_size_bytes)로 연결돼요. - 각 청크는 하나의
start_weight_update/finish_weight_update괄호 안에서 별도의update_weights호출로 보내져서, 청크 수와 무관하게 레이어별 재로드 패스가 시작에 한 번, 끝에 한 번 초기화·종료됩니다. - 각 청크가 소비된 뒤 그 청크의 GPU 메모리를 회수할 수 있어요.
engine = WeightTransferTrainerFactory.trainer_init(
init_info=IPCTrainerInitInfo(
rank=0,
packed=True,
packed_buffer_size_bytes=256 * 1024 * 1024, # 256 MB chunks
),
client=client,
source=ModuleSource(model),
)
멀티 랭크 트레이너에서 생산자는 청크 간에 버퍼를 재사용하므로, packed 모드는 랭크 간에 청크별 barrier를 수반해요. 이것이 없으면 랭크가 컬로케이트된 워커가 현재 청크를 읽는 동안 자기 버퍼를 덮어쓸 수 있거든요. 이는 send_weights() 안에서 처리됩니다.
랭크 로컬 갱신 (Rank-Local Updates)
워커마다 다른 파라미터 부분집합을 위해 update_info는 워커 랭크로 인덱싱된 리스트일 수 있어요. 이 형태는 IPC 고유인데, 집단 백엔드는 모든 워커가 참여해야 하기 때문입니다.
예시 (Examples)
- IPC 가중치 동기화를 쓰는 RLHF (
vllm serve, HTTP) — 여기서 시작하세요. 서버와 훈련 모델이 단일 GPU를 공유; HTTP 컨트롤 플레인, CUDA IPC 데이터 플레인. 자체 서버를 띄웠다가 내립니다. - IPC + FSDP2와 전문가 병렬을 쓰는 RLHF — 같은 4개 GPU에서
--data-parallel-size 4서버와 컬로케이트된 멀티 랭크 트레이너: 모든 FSDP 랭크가 엔진을 만들고 핸들 all-gather에 참여하며, packed 청킹과 전송 주변의 sleep/wake를 사용.