main
py 218 lines 6.81 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for GPUWorker weight-transfer pass-through behavior.
4
5 The worker no longer contains transport, layerwise, or sparse logic: it only
6 delegates to the configured weight transfer engine and tracks whether an update
7 session is active. These tests verify that delegation and the session guard.
8 """
9
10 import pytest
11 import torch
12 import torch.nn as nn
13
14 from vllm.config import ParallelConfig, VllmConfig, get_current_vllm_config
15 from vllm.lora.layers import BaseLayerWithLoRA
16 from vllm.v1.worker.gpu_model_runner import _get_parameter_for_reload
17 from vllm.v1.worker.gpu_worker import Worker
18
19
20 class _RecordingEngine:
21 """Minimal stand-in for a weight transfer engine."""
22
23 def __init__(self, raise_on_update: bool = False):
24 self.raise_on_update = raise_on_update
25 self.started = False
26 self.finished = False
27 self.reset_count = 0
28 self.supports_draft_weight_update = False
29 self.update_calls: list[dict] = []
30 self.seen_configs: list[VllmConfig] = []
31
32 def _record_config(self) -> None:
33 self.seen_configs.append(get_current_vllm_config())
34
35 def start_weight_update(self) -> None:
36 self._record_config()
37 self.started = True
38
39 def update_weights(self, update_info: dict) -> None:
40 self._record_config()
41 self.update_calls.append(update_info)
42 if self.raise_on_update:
43 raise ValueError("boom")
44
45 def finish_weight_update(self) -> None:
46 self._record_config()
47 self.finished = True
48
49 def reset_weight_update_target(self) -> None:
50 self.reset_count += 1
51
52
53 class _RecordingModelRunner:
54 def __init__(self) -> None:
55 self.seen_config: VllmConfig | None = None
56 self.reset_lora_calls = 0
57
58 def reload_weights(self) -> None:
59 self.seen_config = get_current_vllm_config()
60
61 def reset_lora_state(self) -> None:
62 self.reset_lora_calls += 1
63
64
65 def _make_worker(engine: _RecordingEngine | None) -> Worker:
66 worker = object.__new__(Worker)
67 worker.vllm_config = VllmConfig()
68 worker.weight_transfer_engine = engine
69 worker._weight_update_active = False
70 worker._weight_update_is_draft = False
71 worker.model_runner = _RecordingModelRunner()
72 return worker
73
74
75 def test_reload_weights_sets_current_config():
76 worker = _make_worker(None)
77 model_runner = _RecordingModelRunner()
78 worker.model_runner = model_runner
79
80 Worker.reload_weights(worker)
81
82 assert model_runner.seen_config is worker.vllm_config
83
84
85 def test_reload_parameter_lookup_preserves_lora_module_names():
86 base_layer = nn.Module()
87 qweight = nn.Parameter(torch.ones(1))
88 base_layer.register_parameter("qweight", qweight)
89 wrapper = BaseLayerWithLoRA()
90 wrapper.base_layer = base_layer
91 model = nn.Module()
92 model.proj = wrapper
93
94 named_parameters = dict(model.named_parameters())
95 assert set(named_parameters) == {"proj.base_layer.qweight"}
96 assert named_parameters["proj.base_layer.qweight"] is qweight
97 assert model.get_parameter("proj.base_layer.qweight") is qweight
98 assert _get_parameter_for_reload(model, "proj.qweight") is qweight
99
100
101 def test_start_update_finish_delegates_to_engine():
102 engine = _RecordingEngine()
103 worker = _make_worker(engine)
104
105 Worker.start_weight_update(worker)
106 assert engine.started is True
107 assert worker._weight_update_active is True
108
109 Worker.update_weights(worker, {"names": ["w"]})
110 assert engine.update_calls == [{"names": ["w"]}]
111 assert worker._weight_update_active is True
112
113 Worker.finish_weight_update(worker)
114 assert engine.finished is True
115 assert engine.reset_count == 1
116 assert worker._weight_update_active is False
117 assert engine.seen_configs == [worker.vllm_config] * 3
118 assert worker.model_runner.reset_lora_calls == 1
119
120
121 @pytest.mark.parametrize(
122 ("rank", "expected"),
123 [(1, {"names": ["rank-1"]}), (2, {"names": []})],
124 )
125 def test_rank_local_update_selects_worker_payload(rank, expected):
126 engine = _RecordingEngine()
127 worker = _make_worker(engine)
128 worker.rank = rank
129 Worker.start_weight_update(worker)
130
131 Worker.update_weights(
132 worker, [{"names": ["rank-0"]}, {"names": ["rank-1"]}, {"names": []}]
133 )
134
135 assert engine.update_calls == [expected]
136 assert worker._weight_update_active is True
137
138
139 def test_rank_local_update_uses_data_parallel_index_after_reconfigure():
140 engine = _RecordingEngine()
141 worker = _make_worker(engine)
142 worker.rank = 0
143 parallel_config = ParallelConfig(
144 data_parallel_size=4,
145 data_parallel_rank=2,
146 )
147 assert parallel_config.data_parallel_rank == 2
148 assert parallel_config.data_parallel_index == 2
149
150 parallel_config.reconfigure_for_independent_dp_rank()
151 assert parallel_config.data_parallel_rank == 0
152 assert parallel_config.data_parallel_index == 2
153
154 worker.vllm_config.parallel_config = parallel_config
155 Worker.start_weight_update(worker)
156
157 Worker.update_weights(
158 worker,
159 [
160 {"names": ["dp-0"]},
161 {"names": ["dp-1"]},
162 {"names": ["dp-2"]},
163 {"names": ["dp-3"]},
164 ],
165 )
166
167 assert engine.update_calls == [{"names": ["dp-2"]}]
168 assert worker._weight_update_active is True
169
170
171 def test_finish_draft_session_keeps_lora_state():
172 engine = _RecordingEngine()
173 engine.supports_draft_weight_update = True
174 worker = _make_worker(engine)
175 worker._set_draft_weight_update_target = lambda: None
176
177 Worker.start_draft_weight_update(worker)
178 Worker.finish_weight_update(worker)
179
180 assert worker.model_runner.reset_lora_calls == 0
181
182
183 def test_double_start_raises():
184 worker = _make_worker(_RecordingEngine())
185 Worker.start_weight_update(worker)
186 with pytest.raises(RuntimeError, match="already"):
187 Worker.start_weight_update(worker)
188
189
190 def test_update_without_start_raises():
191 worker = _make_worker(_RecordingEngine())
192 with pytest.raises(RuntimeError, match="start_weight_update must be called"):
193 Worker.update_weights(worker, {"names": ["w"]})
194
195
196 def test_finish_without_start_raises():
197 worker = _make_worker(_RecordingEngine())
198 with pytest.raises(RuntimeError, match="without a matching"):
199 Worker.finish_weight_update(worker)
200
201
202 def test_update_resets_active_on_error():
203 engine = _RecordingEngine(raise_on_update=True)
204 worker = _make_worker(engine)
205 Worker.start_weight_update(worker)
206
207 with pytest.raises(ValueError, match="boom"):
208 Worker.update_weights(worker, {"names": ["w"]})
209
210 # A failed update ends the session so the next start is clean.
211 assert engine.reset_count == 1
212 assert worker._weight_update_active is False
213
214
215 def test_missing_engine_raises():
216 worker = _make_worker(None)
217 with pytest.raises(RuntimeError, match="Weight transfer not configured"):
218 Worker.start_weight_update(worker)