main
py 199 lines 6.35 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """RLHF weight syncing against a `vllm serve` HTTP server, using CUDA IPC for the
4 data plane.
5
6 * OpenAI-compatible API for inference requests
7 * HTTP endpoints for the weight-transfer control plane
8 * CUDA IPC handles for the weight data plane
9
10 1-GPU layout (single node): IPC shares GPU memory directly, so the server (TP=1)
11 and the training model both live on GPU 0. The server is started with
12 `--gpu-memory-utilization 0.5` to leave room for the training model.
13
14 The script starts the server itself, then:
15
16 1. Generate over HTTP → gibberish (server started with dummy weights).
17 2. Pause generation, sync real weights trainer → server over IPC, resume.
18 3. Generate again → sensible output.
19
20 IPC handles are pickled for HTTP transport, so both sides need
21 `VLLM_ALLOW_INSECURE_SERIALIZATION=1`; this script sets it for itself and for
22 the server it spawns.
23
24 Run:
25 $ python examples/rl/rlhf_http_ipc.py
26 """
27
28 import os
29 import subprocess
30 import sys
31 import time
32
33 import requests
34 import torch
35 from openai import OpenAI
36 from transformers import AutoModelForCausalLM
37
38 from vllm.distributed.weight_transfer import (
39 HTTPVLLMWeightSyncClient,
40 ModuleSource,
41 WeightTransferTrainerFactory,
42 )
43 from vllm.distributed.weight_transfer.ipc_engine import IPCTrainerInitInfo
44
45 MODEL_NAME = "facebook/opt-125m"
46
47 SERVER_PORT = 8000
48 BASE_URL = f"http://localhost:{SERVER_PORT}"
49
50 # IPC requires colocation: the server and the training model share this GPU.
51 SERVER_DEVICE_IDS = "0"
52 TRAINER_DEVICE = "cuda:0"
53 # Leave room on the shared GPU for the training model.
54 SERVER_GPU_MEMORY_UTILIZATION = 0.5
55
56 # Needed to (de)serialize IPC handles across the HTTP boundary.
57 os.environ["VLLM_ALLOW_INSECURE_SERIALIZATION"] = "1"
58
59 PROMPTS = [
60 "Hello, my name is",
61 "The president of the United States is",
62 "The capital of France is",
63 "The future of AI is",
64 ]
65
66
67 def start_vllm_server() -> subprocess.Popen:
68 """Spawn `vllm serve` and block until it is healthy."""
69 serve_args = [
70 "vllm",
71 "serve",
72 MODEL_NAME,
73 "--tensor-parallel-size",
74 "1",
75 "--device-ids",
76 SERVER_DEVICE_IDS,
77 "--enforce-eager",
78 "--load-format",
79 "dummy",
80 "--gpu-memory-utilization",
81 str(SERVER_GPU_MEMORY_UTILIZATION),
82 "--port",
83 str(SERVER_PORT),
84 "--weight-transfer-config",
85 '{"backend": "ipc"}',
86 ]
87 env = os.environ.copy()
88 # Exposes the weight-transfer and pause/resume endpoints.
89 env["VLLM_SERVER_DEV_MODE"] = "1"
90 env["VLLM_ALLOW_INSECURE_SERIALIZATION"] = "1"
91 print(f"[server] Launching: {' '.join(serve_args)}")
92 proc = subprocess.Popen(
93 serve_args,
94 env=env,
95 stdout=sys.stdout,
96 stderr=sys.stderr,
97 start_new_session=True,
98 )
99
100 deadline = time.monotonic() + 900
101 while True:
102 if proc.poll() is not None:
103 raise RuntimeError("vLLM server exited before becoming ready.")
104 try:
105 if requests.get(f"{BASE_URL}/health", timeout=5).status_code == 200:
106 break
107 except requests.RequestException:
108 pass
109 if time.monotonic() > deadline:
110 raise RuntimeError("vLLM server failed to start in time.")
111 time.sleep(2)
112 print("[server] Ready.")
113 return proc
114
115
116 def generate_completions(client: OpenAI, model: str, prompts: list[str]) -> list[str]:
117 """Generate completions using the OpenAI-compatible API."""
118 results = []
119 for prompt in prompts:
120 response = client.completions.create(
121 model=model,
122 prompt=prompt,
123 max_tokens=32,
124 temperature=0,
125 )
126 results.append(response.choices[0].text)
127 return results
128
129
130 def pause_generation(base_url: str) -> None:
131 """Pause generation via HTTP endpoint."""
132 requests.post(f"{base_url}/pause", timeout=60).raise_for_status()
133
134
135 def resume_generation(base_url: str) -> None:
136 """Resume generation via HTTP endpoint."""
137 requests.post(f"{base_url}/resume", timeout=60).raise_for_status()
138
139
140 def print_generations(label: str, prompts: list[str], outputs: list[str]) -> None:
141 print("-" * 50)
142 print(label)
143 print("-" * 50)
144 for prompt, generated_text in zip(prompts, outputs):
145 print(f"Prompt: {prompt!r}\nGenerated text: {generated_text!r}")
146 print("-" * 50)
147
148
149 def main():
150 server_proc = start_vllm_server()
151 try:
152 # The training model must sit on the same physical GPU as the server.
153 torch.accelerator.set_device_index(TRAINER_DEVICE)
154
155 print(f"[trainer] Loading training model: {MODEL_NAME} on {TRAINER_DEVICE}")
156 train_model = AutoModelForCausalLM.from_pretrained(
157 MODEL_NAME, dtype=torch.bfloat16
158 )
159 train_model.to(TRAINER_DEVICE)
160 train_model.eval() # eval mode to save memory on the shared GPU
161
162 client = OpenAI(base_url=f"{BASE_URL}/v1", api_key="EMPTY")
163
164 # Generate with dummy weights — expect nonsense.
165 outputs = generate_completions(client, MODEL_NAME, PROMPTS)
166 print_generations("BEFORE weight sync (dummy weights):", PROMPTS, outputs)
167
168 # IPC needs no data-plane rendezvous; `trainer_init` only ships the
169 # `packed` flag, which the server must decode with.
170 print("[transfer] Initializing IPC weight transfer...")
171 engine = WeightTransferTrainerFactory.trainer_init(
172 init_info=IPCTrainerInitInfo(rank=0, packed=False), # rank 0 = sender
173 client=HTTPVLLMWeightSyncClient(BASE_URL),
174 source=ModuleSource(train_model),
175 )
176
177 pause_generation(BASE_URL)
178
179 # Drives start_weight_update / update_weights / finish_weight_update.
180 print("[sync] Sharing weights via CUDA IPC...")
181 engine.send_weights()
182 print("[sync] Weight transfer complete.")
183
184 resume_generation(BASE_URL)
185
186 # Generate with the synced weights — expect sensible output.
187 outputs_updated = generate_completions(client, MODEL_NAME, PROMPTS)
188 print_generations("AFTER weight sync (real weights):", PROMPTS, outputs_updated)
189 finally:
190 print("[server] Shutting down...")
191 server_proc.terminate()
192 try:
193 server_proc.wait(timeout=30)
194 except subprocess.TimeoutExpired:
195 server_proc.kill()
196
197
198 if __name__ == "__main__":
199 main()