Close vllm export proof gaps
- SHA
8eba0b91b20bc993f051d7ae68634411b188a81d- Parents
-
c6f6b00 - Tree
266f4d4
8eba0b9
8eba0b91b20bc993f051d7ae68634411b188a81dc6f6b00
266f4d4| Status | File | + | - |
|---|---|---|---|
| M |
src/dlm/cli/commands.py
|
4 | 4 |
| M |
tests/integration/export/_runtime_smoke.py
|
29 | 0 |
| A |
tests/integration/export/test_vllm_smoke.py
|
62 | 0 |
| M |
tests/unit/export/targets/test_vllm_argv.py
|
29 | 0 |
src/dlm/cli/commands.pymodified@@ -1784,13 +1784,13 @@ def export_cmd( | ||
| 1784 | 1784 | if resolved_target.name == "vllm" and export_dispatch.accepts_audio: |
| 1785 | 1785 | console.print( |
| 1786 | 1786 | "[red]export:[/red] --target vllm is not wired for audio-language " |
| 1787 | - "documents yet; this Sprint 41 slice only supports text bases." | |
| 1787 | + "documents yet; the current vllm export path only supports text bases." | |
| 1788 | 1788 | ) |
| 1789 | 1789 | raise typer.Exit(code=2) |
| 1790 | 1790 | if resolved_target.name == "mlx-serve" and export_dispatch.accepts_audio: |
| 1791 | 1791 | console.print( |
| 1792 | 1792 | "[red]export:[/red] --target mlx-serve is not wired for audio-language " |
| 1793 | - "documents yet; this Sprint 41 slice only supports text bases." | |
| 1793 | + "documents yet; the current mlx-serve export path only supports text bases." | |
| 1794 | 1794 | ) |
| 1795 | 1795 | raise typer.Exit(code=2) |
| 1796 | 1796 | if export_dispatch.accepts_audio: |
@@ -1835,13 +1835,13 @@ def export_cmd( | ||
| 1835 | 1835 | if resolved_target.name == "vllm" and export_dispatch.accepts_images: |
| 1836 | 1836 | console.print( |
| 1837 | 1837 | "[red]export:[/red] --target vllm is not wired for vision-language " |
| 1838 | - "documents yet; this Sprint 41 slice only supports text bases." | |
| 1838 | + "documents yet; the current vllm export path only supports text bases." | |
| 1839 | 1839 | ) |
| 1840 | 1840 | raise typer.Exit(code=2) |
| 1841 | 1841 | if resolved_target.name == "mlx-serve" and export_dispatch.accepts_images: |
| 1842 | 1842 | console.print( |
| 1843 | 1843 | "[red]export:[/red] --target mlx-serve is not wired for vision-language " |
| 1844 | - "documents yet; this Sprint 41 slice only supports text bases." | |
| 1844 | + "documents yet; the current mlx-serve export path only supports text bases." | |
| 1845 | 1845 | ) |
| 1846 | 1846 | raise typer.Exit(code=2) |
| 1847 | 1847 | if export_dispatch.accepts_images: |
tests/integration/export/_runtime_smoke.pymodified@@ -2,8 +2,12 @@ | ||
| 2 | 2 | |
| 3 | 3 | from __future__ import annotations |
| 4 | 4 | |
| 5 | +import importlib.util | |
| 5 | 6 | import os |
| 7 | +import platform | |
| 8 | +import shutil | |
| 6 | 9 | import socket |
| 10 | +import sys | |
| 7 | 11 | from collections.abc import Iterator |
| 8 | 12 | from contextlib import contextmanager |
| 9 | 13 | from pathlib import Path |
@@ -26,6 +30,31 @@ def vendor_server_built() -> bool: | ||
| 26 | 30 | return (vendor_root / "build" / "bin" / "llama-server").is_file() |
| 27 | 31 | |
| 28 | 32 | |
| 33 | +def require_safe_vllm_smoke_host() -> None: | |
| 34 | + """Skip when the host/runtime combo is not safe for live vLLM smoke.""" | |
| 35 | + reason = vllm_smoke_skip_reason() | |
| 36 | + if reason is not None: | |
| 37 | + pytest.skip(reason) | |
| 38 | + | |
| 39 | + | |
| 40 | +def vllm_smoke_skip_reason() -> str | None: | |
| 41 | + """Return the skip reason for live vLLM smoke, or None when allowed.""" | |
| 42 | + if shutil.which("vllm") is None: | |
| 43 | + return "vllm CLI not on PATH." | |
| 44 | + if importlib.util.find_spec("vllm") is None: | |
| 45 | + return "vllm Python package not importable." | |
| 46 | + if ( | |
| 47 | + os.environ.get("DLM_RUN_VLLM_SMOKE") != "1" | |
| 48 | + and sys.platform == "darwin" | |
| 49 | + and platform.machine() == "arm64" | |
| 50 | + ): | |
| 51 | + return ( | |
| 52 | + "vllm-metal smoke requires DLM_RUN_VLLM_SMOKE=1 on Apple Silicon; " | |
| 53 | + "engine init can otherwise trigger host-wide memory pressure." | |
| 54 | + ) | |
| 55 | + return None | |
| 56 | + | |
| 57 | + | |
| 29 | 58 | @contextmanager |
| 30 | 59 | def cleared_offline_env() -> Iterator[None]: |
| 31 | 60 | """Temporarily clear the offline HF env so cached snapshots can resolve.""" |
tests/integration/export/test_vllm_smoke.pyadded@@ -0,0 +1,62 @@ | ||
| 1 | +"""Live `vllm` export smoke using the Sprint 14.5 trained store.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import os | |
| 6 | +from typing import TYPE_CHECKING | |
| 7 | + | |
| 8 | +import pytest | |
| 9 | +from typer.testing import CliRunner | |
| 10 | + | |
| 11 | +from tests.integration.export._runtime_smoke import ( | |
| 12 | + cleared_offline_env, | |
| 13 | + require_loopback_bind, | |
| 14 | + vllm_smoke_skip_reason, | |
| 15 | +) | |
| 16 | + | |
| 17 | +if TYPE_CHECKING: | |
| 18 | + from tests.fixtures.trained_store import TrainedStoreHandle | |
| 19 | + | |
| 20 | +_VLLM_SKIP_REASON = vllm_smoke_skip_reason() | |
| 21 | + | |
| 22 | +pytestmark = [ | |
| 23 | + pytest.mark.slow, | |
| 24 | + pytest.mark.skipif(_VLLM_SKIP_REASON is not None, reason=_VLLM_SKIP_REASON or ""), | |
| 25 | +] | |
| 26 | + | |
| 27 | + | |
| 28 | +@pytest.mark.slow | |
| 29 | +def test_export_target_vllm_smokes_live(trained_store: TrainedStoreHandle) -> None: | |
| 30 | + require_loopback_bind() | |
| 31 | + | |
| 32 | + from dlm.cli.app import app | |
| 33 | + from dlm.export.manifest import load_export_manifest | |
| 34 | + from dlm.store.manifest import load_manifest | |
| 35 | + | |
| 36 | + os.environ["DLM_HOME"] = str(trained_store.home) | |
| 37 | + | |
| 38 | + with cleared_offline_env(): | |
| 39 | + runner = CliRunner() | |
| 40 | + result = runner.invoke( | |
| 41 | + app, | |
| 42 | + [ | |
| 43 | + "export", | |
| 44 | + str(trained_store.doc), | |
| 45 | + "--target", | |
| 46 | + "vllm", | |
| 47 | + ], | |
| 48 | + ) | |
| 49 | + | |
| 50 | + assert result.exit_code == 0, result.output | |
| 51 | + | |
| 52 | + export_dir = trained_store.store.exports / "vllm" | |
| 53 | + manifest = load_export_manifest(export_dir) | |
| 54 | + store_manifest = load_manifest(trained_store.store.manifest) | |
| 55 | + | |
| 56 | + assert (export_dir / "vllm_launch.sh").is_file() | |
| 57 | + assert (export_dir / "vllm_config.json").is_file() | |
| 58 | + assert (export_dir / "adapters" / "adapter").is_dir() | |
| 59 | + assert manifest.target == "vllm" | |
| 60 | + assert store_manifest.exports, "store export summary missing" | |
| 61 | + assert store_manifest.exports[-1].target == "vllm" | |
| 62 | + assert store_manifest.exports[-1].smoke_output_first_line | |
tests/unit/export/targets/test_vllm_argv.pymodified@@ -139,6 +139,35 @@ class TestPrepareVllmExport: | ||
| 139 | 139 | {"adapter_version": 4, "name": "tone", "path": "adapters/tone"}, |
| 140 | 140 | ] |
| 141 | 141 | |
| 142 | + def test_adapter_mix_override_stages_one_mixed_module(self, tmp_path: Path) -> None: | |
| 143 | + store = _setup_named_store(tmp_path) | |
| 144 | + mixed = tmp_path / "mixed" | |
| 145 | + _write_adapter(mixed) | |
| 146 | + | |
| 147 | + prepared = prepare_vllm_export( | |
| 148 | + store=store, | |
| 149 | + spec=_SPEC, | |
| 150 | + served_model_name="dlm-mixed", | |
| 151 | + training_sequence_len=1024, | |
| 152 | + adapter_name=None, | |
| 153 | + adapter_path_override=mixed, | |
| 154 | + declared_adapter_names=("knowledge", "tone"), | |
| 155 | + ) | |
| 156 | + | |
| 157 | + script = prepared.launch_script_path.read_text(encoding="utf-8") | |
| 158 | + assert "--served-model-name dlm-mixed" in script | |
| 159 | + assert "--max-model-len 1024" in script | |
| 160 | + assert 'mixed="$SCRIPT_DIR/adapters/mixed"' in script | |
| 161 | + assert 'knowledge="$SCRIPT_DIR/adapters/knowledge"' not in script | |
| 162 | + assert 'tone="$SCRIPT_DIR/adapters/tone"' not in script | |
| 163 | + | |
| 164 | + config = json.loads( | |
| 165 | + (prepared.export_dir / VLLM_CONFIG_FILENAME).read_text(encoding="utf-8") | |
| 166 | + ) | |
| 167 | + assert config["lora_modules"] == [ | |
| 168 | + {"adapter_version": 1, "name": "mixed", "path": "adapters/mixed"} | |
| 169 | + ] | |
| 170 | + | |
| 142 | 171 | def test_apple_silicon_export_records_conservative_runtime_env( |
| 143 | 172 | self, tmp_path: Path, monkeypatch: object |
| 144 | 173 | ) -> None: |