Skip to content

Commit c120dad

Browse files
yilunh998yilunh
andauthored
[BugFix] Fix static_forward_context cleanup and MTP draft loading (vllm-project#11491)
### What this PR does / why we need it? This PR fixes two issues in `ModelNetLoaderElastic` when loading models via D2D netloader, especially in MTP / speculative decoding scenarios. **1. Clear `static_forward_context` before fallback** When elastic loading fails and the loader falls back to `DefaultModelLoader`, `initialize_model` may have already registered attention layers into `compilation_config.static_forward_context`. Without clearing these registrations, `revert_to_default()` can hit duplicate-registration errors or leave stale layer state. Changes: - Add `_get_static_forward_context()` and `_clear_static_forward_context()` helpers. - Clear both the passed `vllm_config` and the global `get_current_vllm_config()` (when available), with deduplication by object id. - Invoke cleanup in the fallback path, right before `revert_to_default()`. **2. Improve MTP draft model netloader efficiency and synchronization** In MTP scenarios, target and draft models are loaded sequentially via netloader. Two problems were observed: - **DRAM cache inefficiency for draft model**: When `INT8_CACHE=dram`, the draft model's `ElasticServer` also cached int8 weights to CPU DRAM. Draft models are typically much smaller; keeping int8 cache in HBM avoids unnecessary CPU↔NPU transfers during weight serving and improves startup/serving efficiency. - **Race across ranks**: Draft model loading could start before all ranks finished target model netloader setup. A `torch.distributed.barrier()` is added after target model loading (only when `speculative_config` is set) so all ranks synchronize before draft loading proceeds. Changes: - Draft `ElasticServer` uses `int8_cache="hbm"` when user config is not `"no"` (overrides `"dram"` for draft only). - Add `_sync_target_netloader_before_draft()` barrier after target model load completes. - Draft model path does not trigger the barrier. fixes vllm-project#11553 ### Does this PR introduce _any_ user-facing change? No API or CLI changes. ### How was this patch tested? Pre-commit passed Unit tests passed E2E verified: Elastic server start time has been significantly reduced under netloader + MTP with 'int8_cache=dram' serving on 4 * Ascend 800I A3 scenario. **fix1** **before:** <img width="1589" height="804" alt="image" src="https://github.com/user-attachments/assets/22c71938-d8ed-4e12-aa2d-6451e71fbad7" /> **after:** <img width="1612" height="570" alt="image" src="https://github.com/user-attachments/assets/07315209-a28b-4649-bdcc-ed92d2f905ca" /> curl coordinator: <img width="1035" height="586" alt="image" src="https://github.com/user-attachments/assets/ad5975c4-b3d7-494b-b539-8824b71dc686" /> **fix2** **before:** <img width="1017" height="503" alt="image" src="https://github.com/user-attachments/assets/155f74f4-a254-4a73-aff7-a203c91c44e2" /> **after:** <img width="1003" height="494" alt="image" src="https://github.com/user-attachments/assets/569efaf8-7562-4826-91f7-d8148047bfab" /> curl coordinator: <img width="1032" height="635" alt="image" src="https://github.com/user-attachments/assets/7a97c969-d5ad-48ef-a9a5-5be484c9a951" /> - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@1f486d9 --------- Signed-off-by: yilunh <hanyilun1@huawei.com> Co-authored-by: yilunh998 <yilunh998@users.noreply.github.com> Co-authored-by: yilunh <hanyilun1@huawei.com>
1 parent beb54c2 commit c120dad

2 files changed

Lines changed: 221 additions & 2 deletions

File tree

tests/ut/model_loader/netloader/test_netloader.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class DummyVllmConfig:
3939
parallel_config = DummyParallelConfig()
4040
additional_config = None
4141
quant_config = None
42+
speculative_config: object | None = None
4243

4344

4445
class DummyModelConfig:
@@ -133,6 +134,30 @@ def test_init_with_invalid_config(monkeypatch):
133134
assert loader.output_prefix is None
134135

135136

137+
def test_clear_static_forward_context_clears_current_vllm_config(monkeypatch):
138+
class DummyCompilationConfig:
139+
def __init__(self):
140+
self.static_forward_context = {}
141+
142+
class ConfigWithCompilation:
143+
def __init__(self):
144+
self.compilation_config = DummyCompilationConfig()
145+
146+
passed_config = ConfigWithCompilation()
147+
current_config = ConfigWithCompilation()
148+
passed_config.compilation_config.static_forward_context["passed.layer"] = object()
149+
current_config.compilation_config.static_forward_context["current.layer"] = object()
150+
monkeypatch.setattr(
151+
"vllm_ascend.model_loader.netloader.netloader.get_current_vllm_config",
152+
lambda: current_config,
153+
)
154+
155+
ModelNetLoaderElastic._clear_static_forward_context(passed_config)
156+
157+
assert passed_config.compilation_config.static_forward_context == {}
158+
assert current_config.compilation_config.static_forward_context == {}
159+
160+
136161
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
137162
def test_load_model_elastic_success(mock_logger, monkeypatch, tmp_path):
138163
monkeypatch.setattr("torch.distributed.get_rank", lambda: 0)
@@ -219,6 +244,100 @@ def __exit__(self, a, b, c):
219244
return dummy_model
220245

221246

247+
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
248+
def test_target_model_waits_for_all_netloader_ranks_before_draft(mock_logger, monkeypatch):
249+
dummy_model = _patch_loader_common(monkeypatch)
250+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.elastic_load", lambda **kwargs: dummy_model)
251+
252+
class DummyElasticServer:
253+
def __init__(*a, **k):
254+
pass
255+
256+
def start(self):
257+
pass
258+
259+
barrier_calls = []
260+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.ElasticServer", DummyElasticServer)
261+
monkeypatch.setattr("torch.distributed.is_available", lambda: True)
262+
monkeypatch.setattr("torch.distributed.is_initialized", lambda: True)
263+
monkeypatch.setattr("torch.distributed.barrier", lambda: barrier_calls.append("barrier"))
264+
265+
extra = {
266+
"SOURCE": [{"device_id": 0, "sources": ["127.0.0.1:5000"]}],
267+
"MODEL": "dummy-model",
268+
"LISTEN_PORT": 5555,
269+
"INT8_CACHE": "dram",
270+
}
271+
loader = make_loader_with_config(extra)
272+
vllm_config = DummyVllmConfig()
273+
vllm_config.speculative_config = object()
274+
loader.load_model(vllm_config, DummyModelConfig())
275+
276+
assert barrier_calls == ["barrier"]
277+
278+
279+
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
280+
def test_failed_target_model_participates_in_barrier_before_error(mock_logger, monkeypatch):
281+
_patch_loader_common(monkeypatch)
282+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.elastic_load", lambda **kwargs: None)
283+
monkeypatch.setattr(
284+
"vllm_ascend.model_loader.netloader.netloader.revert_to_default",
285+
lambda *args, **kwargs: (None, False),
286+
)
287+
288+
barrier_calls = []
289+
monkeypatch.setattr("torch.distributed.is_available", lambda: True)
290+
monkeypatch.setattr("torch.distributed.is_initialized", lambda: True)
291+
monkeypatch.setattr("torch.distributed.barrier", lambda: barrier_calls.append("barrier"))
292+
293+
extra = {
294+
"SOURCE": [{"device_id": 0, "sources": ["127.0.0.1:5000"]}],
295+
"MODEL": "dummy-model",
296+
"LISTEN_PORT": 5555,
297+
"INT8_CACHE": "dram",
298+
}
299+
loader = make_loader_with_config(extra)
300+
vllm_config = DummyVllmConfig()
301+
vllm_config.speculative_config = object()
302+
303+
with pytest.raises(RuntimeError, match="NetLoader elastic loads model fails"):
304+
loader.load_model(vllm_config, DummyModelConfig())
305+
306+
assert barrier_calls == ["barrier"]
307+
308+
309+
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
310+
def test_draft_model_does_not_wait_for_target_netloader_barrier(mock_logger, monkeypatch):
311+
dummy_model = _patch_loader_common(monkeypatch)
312+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.elastic_load", lambda **kwargs: dummy_model)
313+
314+
class DummyElasticServer:
315+
def __init__(*a, **k):
316+
pass
317+
318+
def start(self):
319+
pass
320+
321+
barrier_calls = []
322+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.ElasticServer", DummyElasticServer)
323+
monkeypatch.setattr("torch.distributed.is_available", lambda: True)
324+
monkeypatch.setattr("torch.distributed.is_initialized", lambda: True)
325+
monkeypatch.setattr("torch.distributed.barrier", lambda: barrier_calls.append("barrier"))
326+
327+
extra = {
328+
"SOURCE": [{"device_id": 0, "sources": ["127.0.0.1:5000"]}],
329+
"MODEL": "draft-model",
330+
"LISTEN_PORT": 5555,
331+
"INT8_CACHE": "dram",
332+
}
333+
loader = make_loader_with_config(extra)
334+
vllm_config = DummyVllmConfig()
335+
vllm_config.speculative_config = object()
336+
loader.load_model(vllm_config, DummyDraftModelConfig())
337+
338+
assert barrier_calls == []
339+
340+
222341
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
223342
def test_load_draft_model_elastic_success(mock_logger, monkeypatch, tmp_path):
224343
dummy_model = _patch_loader_common(monkeypatch)
@@ -229,6 +348,7 @@ def test_load_draft_model_elastic_success(mock_logger, monkeypatch, tmp_path):
229348
class DummyElasticServer:
230349
def __init__(self, *args, **kwargs):
231350
elastic_server_instances.append(self)
351+
self.int8_cache = args[7]
232352
self.group_name = kwargs.get("group_name")
233353

234354
def start(self):
@@ -248,10 +368,41 @@ def start(self):
248368

249369
assert isinstance(result, nn.Module)
250370
assert loader._draft_elastic_server is elastic_server_instances[0]
371+
assert loader._draft_elastic_server.int8_cache == "no"
251372
assert loader._draft_elastic_server.group_name == "netloader_draft"
252373
assert not (tmp_path / "output_0.txt").exists()
253374

254375

376+
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
377+
def test_load_draft_model_uses_hbm_when_int8_cache_is_dram(mock_logger, monkeypatch):
378+
dummy_model = _patch_loader_common(monkeypatch)
379+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.elastic_load", lambda **kwargs: dummy_model)
380+
381+
captured = {}
382+
383+
class DummyElasticServer:
384+
def __init__(self, *args, **kwargs):
385+
captured["int8_cache"] = args[7]
386+
captured["group_name"] = kwargs.get("group_name")
387+
388+
def start(self):
389+
pass
390+
391+
monkeypatch.setattr("vllm_ascend.model_loader.netloader.netloader.ElasticServer", DummyElasticServer)
392+
393+
extra = {
394+
"SOURCE": [{"device_id": 0, "sources": ["127.0.0.1:5000"]}],
395+
"MODEL": "draft-model",
396+
"LISTEN_PORT": 5555,
397+
"INT8_CACHE": "dram",
398+
}
399+
loader = make_loader_with_config(extra)
400+
loader.load_model(DummyVllmConfig(), DummyDraftModelConfig())
401+
402+
assert captured["int8_cache"] == "hbm"
403+
assert captured["group_name"] == "netloader_draft"
404+
405+
255406
@patch("vllm_ascend.model_loader.netloader.netloader.logger")
256407
def test_load_draft_model_port_offset_and_group_name(mock_logger, monkeypatch, tmp_path):
257408
dummy_model = _patch_loader_common(monkeypatch)

vllm_ascend/model_loader/netloader/netloader.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535

3636
DRAFT_PORT_OFFSET = 10000
3737

38+
try:
39+
# Older vLLM versions may not expose the current-config accessor.
40+
from vllm.config import get_current_vllm_config
41+
except ImportError:
42+
get_current_vllm_config = None
43+
3844

3945
@register_model_loader("netloader")
4046
class ModelNetLoaderElastic(BaseModelLoader):
@@ -136,6 +142,61 @@ def _is_draft_model(model_config: ModelConfig) -> bool:
136142
"""Check whether the model_config corresponds to a draft model for speculative decoding."""
137143
return getattr(model_config, "runner_type", None) == "draft"
138144

145+
@staticmethod
146+
def _sync_target_netloader_before_draft(vllm_config: VllmConfig) -> None:
147+
if getattr(vllm_config, "speculative_config", None) is None:
148+
return
149+
if not torch.distributed.is_available() or not torch.distributed.is_initialized():
150+
return
151+
152+
logger.info("Waiting for all target netloader ranks before loading draft model")
153+
barrier_start = time.perf_counter()
154+
torch.distributed.barrier()
155+
logger.info(
156+
"Target netloader barrier before draft model time: %s",
157+
time.perf_counter() - barrier_start,
158+
)
159+
160+
@staticmethod
161+
def _get_static_forward_context(vllm_config: VllmConfig):
162+
compilation_config = getattr(vllm_config, "compilation_config", None)
163+
static_forward_context = getattr(compilation_config, "static_forward_context", None)
164+
if static_forward_context is None or not hasattr(static_forward_context, "clear"):
165+
return None
166+
return static_forward_context
167+
168+
@staticmethod
169+
def _clear_static_forward_context(vllm_config: VllmConfig) -> None:
170+
"""Clear static layer registrations before rebuilding the model on fallback."""
171+
candidates = [("vllm_config", vllm_config)]
172+
if get_current_vllm_config is not None:
173+
try:
174+
candidates.append(("current_vllm_config", get_current_vllm_config()))
175+
except Exception as e:
176+
logger.debug("Failed to get current vLLM config while clearing static context: %s", e)
177+
178+
cleared_contexts = []
179+
seen_context_ids = set()
180+
for source, config in candidates:
181+
static_forward_context = ModelNetLoaderElastic._get_static_forward_context(config)
182+
if static_forward_context is None:
183+
continue
184+
185+
context_id = id(static_forward_context)
186+
if context_id in seen_context_ids:
187+
continue
188+
seen_context_ids.add(context_id)
189+
190+
try:
191+
context_size = str(len(static_forward_context))
192+
except TypeError:
193+
context_size = "unknown"
194+
static_forward_context.clear()
195+
cleared_contexts.append(f"{source}:{context_size}")
196+
197+
if cleared_contexts:
198+
logger.info("Cleared static_forward_context before fallback: %s", cleared_contexts)
199+
139200
def load_model(self, vllm_config: VllmConfig, model_config: ModelConfig, prefix: str = "") -> nn.Module:
140201
"""
141202
Loads the model using the specified configuration.
@@ -240,6 +301,9 @@ def load_model(self, vllm_config: VllmConfig, model_config: ModelConfig, prefix:
240301
logger.info("Empty CUDA cache")
241302
torch.cuda.empty_cache()
242303

304+
# Clear registrations from the failed initialize_model
305+
self._clear_static_forward_context(vllm_config)
306+
243307
model, need_process_weights_after_loading = self.revert_to_default(
244308
model_config, vllm_config, device_config, prefix
245309
)
@@ -293,6 +357,7 @@ def load_model(self, vllm_config: VllmConfig, model_config: ModelConfig, prefix:
293357
logger.error("Unknown error: %s", e)
294358

295359
try:
360+
server_int8_cache = "hbm" if is_draft and self.int8_cache != "no" else self.int8_cache
296361
elastic_server = ElasticServer(
297362
driver_ip,
298363
listen_port,
@@ -301,7 +366,7 @@ def load_model(self, vllm_config: VllmConfig, model_config: ModelConfig, prefix:
301366
model_config.model,
302367
parallel_config.tensor_parallel_size,
303368
parallel_config.pipeline_parallel_size,
304-
self.int8_cache,
369+
server_int8_cache,
305370
self.int8_cache_name,
306371
group_name=group_name,
307372
)
@@ -321,9 +386,12 @@ def load_model(self, vllm_config: VllmConfig, model_config: ModelConfig, prefix:
321386
if need_process_weights_after_loading:
322387
process_weights_after_loading(model, model_config, torch.device(device_config.device))
323388

389+
if not is_draft:
390+
self._sync_target_netloader_before_draft(vllm_config)
391+
324392
if model is None:
325393
logger.error("NetLoader elastic loads model fails")
326-
return None
394+
raise RuntimeError("NetLoader elastic loads model fails")
327395

328396
return model.eval()
329397

0 commit comments

Comments
 (0)