tenseleyflow/documentlanguagemodel / 8eba0b9

Browse files

Close vllm export proof gaps

Authored by espadonne
SHA
8eba0b91b20bc993f051d7ae68634411b188a81d
Parents
c6f6b00
Tree
266f4d4

4 changed files

StatusFile+-
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(
17841784
     if resolved_target.name == "vllm" and export_dispatch.accepts_audio:
17851785
         console.print(
17861786
             "[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."
17881788
         )
17891789
         raise typer.Exit(code=2)
17901790
     if resolved_target.name == "mlx-serve" and export_dispatch.accepts_audio:
17911791
         console.print(
17921792
             "[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."
17941794
         )
17951795
         raise typer.Exit(code=2)
17961796
     if export_dispatch.accepts_audio:
@@ -1835,13 +1835,13 @@ def export_cmd(
18351835
     if resolved_target.name == "vllm" and export_dispatch.accepts_images:
18361836
         console.print(
18371837
             "[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."
18391839
         )
18401840
         raise typer.Exit(code=2)
18411841
     if resolved_target.name == "mlx-serve" and export_dispatch.accepts_images:
18421842
         console.print(
18431843
             "[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."
18451845
         )
18461846
         raise typer.Exit(code=2)
18471847
     if export_dispatch.accepts_images:
tests/integration/export/_runtime_smoke.pymodified
@@ -2,8 +2,12 @@
22
 
33
 from __future__ import annotations
44
 
5
+import importlib.util
56
 import os
7
+import platform
8
+import shutil
69
 import socket
10
+import sys
711
 from collections.abc import Iterator
812
 from contextlib import contextmanager
913
 from pathlib import Path
@@ -26,6 +30,31 @@ def vendor_server_built() -> bool:
2630
     return (vendor_root / "build" / "bin" / "llama-server").is_file()
2731
 
2832
 
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
+
2958
 @contextmanager
3059
 def cleared_offline_env() -> Iterator[None]:
3160
     """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:
139139
             {"adapter_version": 4, "name": "tone", "path": "adapters/tone"},
140140
         ]
141141
 
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
+
142171
     def test_apple_silicon_export_records_conservative_runtime_env(
143172
         self, tmp_path: Path, monkeypatch: object
144173
     ) -> None: