Repository navigation
Fast Start #54921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,529
−13
Merged
Fast Start #54921
Changes from 4 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f8d3c8b
add
bead456
update
826e603
Merge remote-tracking branch 'upstream/main' into fast_start
64d1f57
fix
fbe836b
use e2e test
Isotr0py e8942ca
fix
liusy58 f4181cd
fix
liusy58 13c588c
fix
liusy58 709d48b
fix
liusy58 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,360 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Tests for the weight cache daemon protocol and the IPC model loader.""" | ||
|
|
||
| import socket | ||
| import threading | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm.config.load import LoadConfig | ||
| from vllm.model_executor.model_loader import get_model_loader | ||
| from vllm.model_executor.model_loader.weight_cache import ( | ||
| CacheConfig, | ||
| CacheConfigMismatchError, | ||
| IpcModelLoader, | ||
| TensorEntry, | ||
| UnsupportedQuantForIPCError, | ||
| WeightCacheUnavailableError, | ||
| is_ipc_quant_supported, | ||
| ) | ||
| from vllm.model_executor.model_loader.weight_cache.protocol import ( | ||
| recv_msg, | ||
| send_msg, | ||
| ) | ||
|
|
||
|
|
||
| def _make_cache_config(**overrides) -> CacheConfig: | ||
| kwargs = dict( | ||
| model="test-model", | ||
| model_arch="LlamaForCausalLM", | ||
| tp_size=2, | ||
| tp_rank=0, | ||
| dtype="torch.bfloat16", | ||
| quantization=None, | ||
| quant_config_hash="", | ||
| revision=None, | ||
| vllm_version="test", | ||
| ) | ||
| kwargs.update(overrides) | ||
| return CacheConfig(**kwargs) | ||
|
|
||
|
|
||
| def test_cache_config_match(): | ||
| assert _make_cache_config().mismatched_fields(_make_cache_config()) == [] | ||
|
|
||
|
|
||
| def test_cache_config_mismatch(): | ||
| other = _make_cache_config(tp_rank=1, dtype="torch.float16") | ||
| assert _make_cache_config().mismatched_fields(other) == ["tp_rank", "dtype"] | ||
|
|
||
|
|
||
| def test_protocol_roundtrip(): | ||
| left, right = socket.socketpair() | ||
| with left, right: | ||
| msg = {"cmd": "get_state", "cache_config": _make_cache_config()} | ||
| send_msg(left, msg) | ||
| assert recv_msg(right) == msg | ||
|
|
||
|
|
||
| def test_get_model_loader_dispatch(): | ||
| loader = get_model_loader(LoadConfig(load_format="ipc_cache")) | ||
| assert isinstance(loader, IpcModelLoader) | ||
| assert loader.mode == "zero_copy" | ||
| assert loader.fallback | ||
|
|
||
|
|
||
| def test_invalid_mode_rejected(): | ||
| with pytest.raises(ValueError, match="mode"): | ||
| IpcModelLoader( | ||
| LoadConfig( | ||
| load_format="ipc_cache", | ||
| model_loader_extra_config={"mode": "bogus"}, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def test_unexpected_extra_config_rejected(): | ||
| with pytest.raises(ValueError, match="Unexpected extra config"): | ||
| IpcModelLoader( | ||
| LoadConfig( | ||
| load_format="ipc_cache", | ||
| model_loader_extra_config={"bogus": 1}, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def test_fallback_load_config_resets_format(): | ||
| loader = IpcModelLoader( | ||
| LoadConfig( | ||
| load_format="ipc_cache", | ||
| model_loader_extra_config={"mode": "copy", "fallback": False}, | ||
| ) | ||
| ) | ||
| fallback_config = loader._fallback_load_config() | ||
| assert fallback_config.load_format == "auto" | ||
| assert fallback_config.model_loader_extra_config == {} | ||
|
|
||
|
|
||
| def test_unavailable_daemon_raises(tmp_path): | ||
| loader = IpcModelLoader( | ||
| LoadConfig( | ||
| load_format="ipc_cache", | ||
| model_loader_extra_config={ | ||
| "socket_path": str(tmp_path / "missing.sock"), | ||
| "fallback": False, | ||
| }, | ||
| ) | ||
| ) | ||
| with pytest.raises(WeightCacheUnavailableError): | ||
| loader._request_state(_make_cache_config()) | ||
|
|
||
|
|
||
| def _serve_one_response(server: socket.socket, response: dict) -> None: | ||
| conn, _ = server.accept() | ||
| with conn: | ||
| recv_msg(conn) | ||
| send_msg(conn, response) | ||
|
|
||
|
|
||
| def _request_against_fake_daemon(tmp_path, response: dict): | ||
| socket_path = str(tmp_path / "daemon.sock") | ||
| server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| server.bind(socket_path) | ||
| server.listen() | ||
| thread = threading.Thread(target=_serve_one_response, args=(server, response)) | ||
| thread.start() | ||
| try: | ||
| loader = IpcModelLoader( | ||
| LoadConfig( | ||
| load_format="ipc_cache", | ||
| model_loader_extra_config={ | ||
| "socket_path": socket_path, | ||
| "fallback": False, | ||
| }, | ||
| ) | ||
| ) | ||
| return loader._request_state(_make_cache_config()) | ||
| finally: | ||
| thread.join(timeout=10) | ||
| server.close() | ||
|
|
||
|
|
||
| def test_mismatch_response_raises(tmp_path): | ||
| with pytest.raises(CacheConfigMismatchError, match="tp_rank"): | ||
| _request_against_fake_daemon( | ||
| tmp_path, {"status": "mismatch", "fields": ["tp_rank"]} | ||
| ) | ||
|
|
||
|
|
||
| def test_error_response_raises(tmp_path): | ||
| with pytest.raises(WeightCacheUnavailableError, match="released"): | ||
| _request_against_fake_daemon( | ||
| tmp_path, {"status": "error", "message": "Weights were released"} | ||
| ) | ||
|
|
||
|
|
||
| def test_ok_response_returns_entries(tmp_path): | ||
| entry = TensorEntry.from_tensor(torch.arange(4, dtype=torch.float32), "param") | ||
| entries, aliases = _request_against_fake_daemon( | ||
| tmp_path, {"status": "ok", "entries": {"layer.weight": entry}} | ||
| ) | ||
| assert aliases == {} | ||
| assert torch.equal( | ||
| entries["layer.weight"].rebuild(0), | ||
| torch.arange(4, dtype=torch.float32), | ||
| ) | ||
|
|
||
|
|
||
| class _TiedModel(torch.nn.Module): | ||
| def __init__(self): | ||
| super().__init__() | ||
| self.embed = torch.nn.Embedding(4, 3) | ||
| self.lm_head = torch.nn.Linear(3, 4, bias=False) | ||
| self.lm_head.weight = self.embed.weight | ||
|
|
||
|
|
||
| def test_export_entries_records_tied_alias(): | ||
| from vllm.model_executor.model_loader.weight_cache.daemon import export_entries | ||
|
|
||
| entries, aliases = export_entries(_TiedModel()) | ||
| assert "embed.weight" in entries | ||
| assert "lm_head.weight" not in entries | ||
| assert aliases == {"lm_head.weight": "embed.weight"} | ||
|
|
||
|
|
||
| def test_apply_entries_restores_tied_identity(): | ||
| from vllm.model_executor.model_loader.weight_cache.daemon import export_entries | ||
|
|
||
| src = _TiedModel() | ||
| entries, aliases = export_entries(src) | ||
|
|
||
| dst = _TiedModel() | ||
| # Break the tie so the two names point at distinct parameters, mimicking a | ||
| # freshly initialized model before the cached weights are applied. | ||
| dst.lm_head.weight = torch.nn.Parameter(torch.zeros(4, 3)) | ||
|
|
||
| loader = IpcModelLoader(LoadConfig(load_format="ipc_cache")) | ||
| loader._apply_entries(dst, entries, aliases, device_index=0) | ||
|
|
||
| assert dst.lm_head.weight is dst.embed.weight | ||
| assert torch.equal(dst.embed.weight, src.embed.weight) | ||
|
|
||
|
|
||
| class _SharedModuleModel(torch.nn.Module): | ||
| """Tied head that reuses the embedding *module*, as vLLM models do.""" | ||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
| self.embed = torch.nn.Embedding(4, 3) | ||
| self.lm_head = self.embed | ||
|
|
||
|
|
||
| def test_apply_entries_handles_shared_module_alias(): | ||
| from vllm.model_executor.model_loader.weight_cache.daemon import export_entries | ||
|
|
||
| src = _SharedModuleModel() | ||
| entries, aliases = export_entries(src) | ||
| assert aliases == {"lm_head.weight": "embed.weight"} | ||
|
|
||
| dst = _SharedModuleModel() | ||
| loader = IpcModelLoader(LoadConfig(load_format="ipc_cache")) | ||
| # "lm_head" is absent from the deduplicated named_modules view, so this | ||
| # used to fail with "Cached tensor lm_head.weight has no matching module". | ||
| loader._apply_entries(dst, entries, aliases, device_index=0) | ||
|
|
||
| assert dst.lm_head.weight is dst.embed.weight | ||
| assert torch.equal(dst.embed.weight, src.embed.weight) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "quantization,quant_config,expected", | ||
| [ | ||
| (None, None, True), | ||
| ("fp8", {"weight_block_size": [128, 128]}, True), | ||
| ("fp8", {}, False), # per-tensor FP8 transposes layer.weight | ||
| ("awq", {}, False), | ||
| ("compressed-tensors", {}, False), | ||
| ], | ||
| ) | ||
| def test_ipc_quant_allowlist(quantization, quant_config, expected): | ||
| assert is_ipc_quant_supported(quantization, quant_config) is expected | ||
|
|
||
|
|
||
| def test_check_supported_rejects_unverified_quant(): | ||
| from types import SimpleNamespace | ||
|
|
||
| loader = IpcModelLoader(LoadConfig(load_format="ipc_cache")) | ||
| model_config = SimpleNamespace( | ||
| quantization="awq", hf_config=SimpleNamespace(quantization_config={}) | ||
| ) | ||
| vllm_config = SimpleNamespace(cache_config=SimpleNamespace(cache_dtype="auto")) | ||
|
|
||
| with pytest.raises(UnsupportedQuantForIPCError, match="awq"): | ||
| loader._check_supported(vllm_config, model_config) | ||
|
|
||
|
|
||
| def test_check_supported_rejects_quantized_kv_cache(): | ||
| from types import SimpleNamespace | ||
|
|
||
| loader = IpcModelLoader(LoadConfig(load_format="ipc_cache")) | ||
| model_config = SimpleNamespace( | ||
| quantization=None, hf_config=SimpleNamespace(quantization_config=None) | ||
| ) | ||
| vllm_config = SimpleNamespace(cache_config=SimpleNamespace(cache_dtype="int8")) | ||
|
|
||
| with pytest.raises(UnsupportedQuantForIPCError, match="kv cache dtype"): | ||
| loader._check_supported(vllm_config, model_config) | ||
|
|
||
|
|
||
| def _make_model_with_method(method) -> torch.nn.Module: | ||
| model = torch.nn.Module() | ||
| model.linear = torch.nn.Linear(2, 2) | ||
| model.linear.quant_method = method | ||
| return model | ||
|
|
||
|
|
||
| def _fake_model_config(): | ||
| from types import SimpleNamespace | ||
|
|
||
| return SimpleNamespace( | ||
| word_embeddings_untied_by_checkpoint=False, | ||
| quantization=None, | ||
| dtype=torch.float32, | ||
| ) | ||
|
|
||
|
|
||
| def test_pre_processed_mode_rejects_undeclared_method(): | ||
| from vllm.model_executor.layers.quantization.base_config import ( | ||
| QuantizeMethodBase, | ||
| ) | ||
| from vllm.model_executor.model_loader.utils import process_weights_after_loading | ||
| from vllm.model_executor.utils import weights_already_processed | ||
|
|
||
| class _Method(QuantizeMethodBase): | ||
| def create_weights(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| def apply(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| model = _make_model_with_method(_Method()) | ||
|
|
||
| with ( | ||
| weights_already_processed(), | ||
| pytest.raises(RuntimeError, match="_Method"), | ||
| ): | ||
| process_weights_after_loading(model, _fake_model_config(), torch.device("cpu")) | ||
|
|
||
|
|
||
| def test_pre_processed_mode_allows_declared_method(): | ||
| from vllm.model_executor.layers.quantization.base_config import ( | ||
| QuantizeMethodBase, | ||
| ) | ||
| from vllm.model_executor.model_loader.utils import process_weights_after_loading | ||
| from vllm.model_executor.utils import ( | ||
| is_weights_pre_processed, | ||
| weights_already_processed, | ||
| ) | ||
|
|
||
| class _Method(QuantizeMethodBase): | ||
| supports_pre_processed_weights = True | ||
|
|
||
| def create_weights(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| def apply(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| def process_weights_after_loading(self, layer): | ||
| layer.saw_pre_processed_flag = is_weights_pre_processed() | ||
|
|
||
| model = _make_model_with_method(_Method()) | ||
| with weights_already_processed(): | ||
| process_weights_after_loading(model, _fake_model_config(), torch.device("cpu")) | ||
|
|
||
| assert model.linear.saw_pre_processed_flag is True | ||
|
|
||
|
|
||
| def _ipc_producer(conn, done) -> None: | ||
| tensor = torch.arange(16, dtype=torch.float32, device="cuda").reshape(4, 4) | ||
| conn.send(TensorEntry.from_tensor(tensor, "param")) | ||
| done.wait(timeout=60) | ||
|
|
||
|
|
||
| @pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA") | ||
| def test_tensor_entry_cross_process_ipc(): | ||
| ctx = torch.multiprocessing.get_context("spawn") | ||
| parent_conn, child_conn = ctx.Pipe() | ||
| done = ctx.Event() | ||
| proc = ctx.Process(target=_ipc_producer, args=(child_conn, done)) | ||
| proc.start() | ||
| try: | ||
| entry = parent_conn.recv() | ||
| tensor = entry.rebuild(torch.cuda.current_device()) | ||
| expected = torch.arange(16, dtype=torch.float32).reshape(4, 4) | ||
| assert torch.equal(tensor.cpu(), expected) | ||
| finally: | ||
| done.set() | ||
| proc.join(timeout=60) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid
torch.cudausage and usetorch.acceleratorapi?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done