3062 lines
103 KiB
Python
3062 lines
103 KiB
Python
"""Tests for curated, resident multimodal LLM inference."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import json
|
|
import logging
|
|
import sys
|
|
import threading
|
|
from dataclasses import dataclass, replace
|
|
from fractions import Fraction
|
|
from io import BytesIO, StringIO
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any, Callable
|
|
|
|
import pytest
|
|
import torch
|
|
from PIL import Image
|
|
|
|
|
|
def test_accurate_triton_listener_distinguishes_persistent_cache_hits(
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""The vLLM EngineCore wrapper should signal only genuine compilations."""
|
|
signal_path = tmp_path / "triton-compile-misses.jsonl"
|
|
status_path = tmp_path / "triton-listeners.jsonl"
|
|
previous_events: list[dict[str, Any]] = []
|
|
engine_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
|
|
fake_triton = SimpleNamespace(
|
|
knobs=SimpleNamespace(
|
|
compilation=SimpleNamespace(
|
|
listener=lambda **event: previous_events.append(event)
|
|
)
|
|
)
|
|
)
|
|
original_setup_hook = object()
|
|
fake_jit_monitor = SimpleNamespace(
|
|
_setup_triton_jit_hook=original_setup_hook,
|
|
)
|
|
|
|
class FakeEngineCoreProc:
|
|
"""Expose the spawn entrypoint patched by the Modal runtime."""
|
|
|
|
@staticmethod
|
|
def run_engine_core(*args: Any, **kwargs: Any) -> str:
|
|
"""Record execution after the child listener is installed."""
|
|
engine_calls.append((args, kwargs))
|
|
return "engine-complete"
|
|
|
|
fake_engine_core = SimpleNamespace(EngineCoreProc=FakeEngineCoreProc)
|
|
|
|
def import_module(name: str) -> Any:
|
|
"""Return the two runtime modules used by listener installation."""
|
|
if name == "triton":
|
|
return fake_triton
|
|
if name == "vllm.utils.jit_monitor":
|
|
return fake_jit_monitor
|
|
if name == "vllm.v1.engine.core":
|
|
return fake_engine_core
|
|
raise ModuleNotFoundError(name)
|
|
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_COMPILE_MISS_SIGNAL_PATH",
|
|
signal_path,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_COMPILE_LISTENER_STATUS_PATH",
|
|
status_path,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_COMPILE_LISTENER_INSTALLED_PID",
|
|
None,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_ENGINE_CORE_LISTENER_RECORDED_PID",
|
|
None,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_VLLM_ENGINE_CORE_ENTRYPOINT_PATCHED_PID",
|
|
None,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_VLLM_ENGINE_CORE_ORIGINAL_ENTRYPOINT",
|
|
None,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module.importlib,
|
|
"import_module",
|
|
import_module,
|
|
)
|
|
modal_llm_runtime_module._install_accurate_triton_compile_listener()
|
|
result = fake_engine_core.EngineCoreProc.run_engine_core("spawned")
|
|
|
|
listener = fake_triton.knobs.compilation.listener
|
|
compile_times = SimpleNamespace(
|
|
ir_initialization=1200,
|
|
lowering_stages=[("ttir", 2300), ("cubin", 3400)],
|
|
store_results=500,
|
|
total=7400,
|
|
)
|
|
common_event = {
|
|
"src": SimpleNamespace(name="example_kernel"),
|
|
"metadata": {"hash": "artifact-1"},
|
|
"times": compile_times,
|
|
}
|
|
with caplog.at_level(logging.INFO):
|
|
listener(**common_event, cache_hit=True)
|
|
assert modal_llm_runtime_module.triton_compile_miss_signal_size() == 0
|
|
assert "cache_hit=True" in caplog.text
|
|
|
|
listener(**common_event, cache_hit=False)
|
|
|
|
assert modal_llm_runtime_module.triton_compile_miss_signal_size() > 0
|
|
miss_event = json.loads(signal_path.read_text(encoding="utf-8"))
|
|
assert miss_event["cache_hit"] is False
|
|
assert miss_event["kernel"] == "example_kernel"
|
|
assert miss_event["timing"]["total_ms"] == 7.4
|
|
assert len(previous_events) == 2
|
|
assert fake_jit_monitor._setup_triton_jit_hook is not original_setup_hook
|
|
assert result == "engine-complete"
|
|
assert engine_calls == [(("spawned",), {})]
|
|
assert modal_llm_runtime_module.triton_compile_listener_engine_pids() == (
|
|
modal_llm_runtime_module.os.getpid(),
|
|
)
|
|
|
|
|
|
def test_spawned_engine_core_recovers_original_entrypoint_and_installs_listener(
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A spawn-fresh module must install telemetry before EngineCore startup."""
|
|
startup_observations: list[tuple[int, ...]] = []
|
|
fake_triton = SimpleNamespace(
|
|
knobs=SimpleNamespace(compilation=SimpleNamespace(listener=None))
|
|
)
|
|
fake_jit_monitor = SimpleNamespace(_setup_triton_jit_hook=lambda: None)
|
|
|
|
class FakeEngineCoreProc:
|
|
"""Represent vLLM's unpatched class in a spawned interpreter."""
|
|
|
|
@staticmethod
|
|
def run_engine_core() -> str:
|
|
"""Observe listener readiness at the first EngineCore instruction."""
|
|
startup_observations.append(
|
|
modal_llm_runtime_module.triton_compile_listener_engine_pids()
|
|
)
|
|
return "started"
|
|
|
|
modules = {
|
|
"triton": fake_triton,
|
|
"vllm.utils.jit_monitor": fake_jit_monitor,
|
|
"vllm.v1.engine.core": SimpleNamespace(EngineCoreProc=FakeEngineCoreProc),
|
|
}
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module.importlib,
|
|
"import_module",
|
|
lambda name: modules[name],
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_COMPILE_LISTENER_STATUS_PATH",
|
|
tmp_path / "listeners.jsonl",
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_COMPILE_LISTENER_INSTALLED_PID",
|
|
None,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_TRITON_ENGINE_CORE_LISTENER_RECORDED_PID",
|
|
None,
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_runtime_module,
|
|
"_VLLM_ENGINE_CORE_ORIGINAL_ENTRYPOINT",
|
|
None,
|
|
)
|
|
|
|
result = (
|
|
modal_llm_runtime_module._run_vllm_engine_core_with_accurate_triton_listener()
|
|
)
|
|
|
|
assert result == "started"
|
|
assert startup_observations == [(modal_llm_runtime_module.os.getpid(),)]
|
|
assert fake_triton.knobs.compilation.listener._comfy_modal_cache_aware
|
|
|
|
|
|
def _text_file(filename: str, text: str) -> dict[str, str]:
|
|
"""Return one built-in-compatible OpenAI input-file payload."""
|
|
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
|
|
return {
|
|
"filename": filename,
|
|
"file_data": f"data:text/plain;base64,{encoded}",
|
|
"type": "input_file",
|
|
}
|
|
|
|
|
|
def test_curated_profile_is_revision_pinned_and_found_in_nested_payload(
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""The registry should reject drift across split-payload discovery."""
|
|
profiles = llm_profiles_module.load_llm_profiles()
|
|
profile = profiles["smolvlm2-2.2b-instruct"]
|
|
|
|
assert profile.revision == "482adb537c021c86670beed01cd58990d01e72e4"
|
|
assert profile.modalities == frozenset({"text", "image", "video", "file"})
|
|
assert profile.trust_remote_code is False
|
|
payload = {
|
|
"split_proxy_payloads": [
|
|
{
|
|
"subgraph_prompt": {
|
|
"12": {
|
|
"class_type": llm_profiles_module.MODAL_LLM_NODE_ID,
|
|
"inputs": {"model_profile": profile.profile_id},
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
assert llm_profiles_module.llm_profile_ids_from_payload(payload) == (
|
|
profile.profile_id,
|
|
)
|
|
|
|
|
|
def test_profile_registry_rejects_mutable_revision(
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""Curated profiles must use an immutable commit rather than main."""
|
|
with pytest.raises(ValueError, match="exact 40-character"):
|
|
llm_profiles_module.LLMModelProfile.from_mapping(
|
|
{
|
|
"id": "unsafe",
|
|
"repository": "owner/model",
|
|
"revision": "main",
|
|
"dtype": "bfloat16",
|
|
"modalities": ["text"],
|
|
"estimated_vram_gb": 1,
|
|
"max_context_tokens": 1024,
|
|
"max_images": 1,
|
|
"max_video_frames": 1,
|
|
"max_file_bytes": 1,
|
|
"max_file_characters": 1,
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("architecture", "quantization_config", "backend", "quantization_method"),
|
|
[
|
|
(
|
|
"Qwen3_5ForConditionalGeneration",
|
|
{"quant_method": "fp8", "fmt": "e4m3"},
|
|
"vllm",
|
|
"fp8",
|
|
),
|
|
(
|
|
"Qwen3_5ForConditionalGeneration",
|
|
{"quant_method": "modelopt", "quant_algo": "NVFP4"},
|
|
"vllm",
|
|
"modelopt_fp4",
|
|
),
|
|
(
|
|
"MuseGlimmerForConditionalGeneration",
|
|
{},
|
|
"transformers",
|
|
"none",
|
|
),
|
|
],
|
|
)
|
|
def test_compatibility_policy_selects_requested_model_backends(
|
|
llm_compatibility_module: Any,
|
|
architecture: str,
|
|
quantization_config: dict[str, str],
|
|
backend: str,
|
|
quantization_method: str,
|
|
) -> None:
|
|
"""The three live-canary model families should resolve before weight download."""
|
|
decision = llm_compatibility_module.resolve_compatibility(
|
|
{
|
|
"architectures": [architecture],
|
|
"dtype": "bfloat16",
|
|
"text_config": {"max_position_embeddings": 262144},
|
|
"quantization_config": quantization_config,
|
|
},
|
|
artifact_bytes=32 * 1024**3,
|
|
)
|
|
|
|
assert decision.backend == backend
|
|
assert decision.quantization_method == quantization_method
|
|
assert decision.default_context_tokens == 32768
|
|
assert decision.advertised_context_tokens == 262144
|
|
assert decision.reasoning_parser == ("qwen3" if backend == "vllm" else "none")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("architecture", "quantization", "reasoning_parser"),
|
|
[
|
|
("SmolVLMForConditionalGeneration", {}, "none"),
|
|
("MuseGlimmerForConditionalGeneration", {}, "none"),
|
|
(
|
|
"Qwen3_5ForConditionalGeneration",
|
|
{"bits": 4, "group_size": 64, "mode": "affine"},
|
|
"qwen3",
|
|
),
|
|
],
|
|
)
|
|
def test_apple_local_compatibility_selects_mlx_vlm(
|
|
llm_compatibility_module: Any,
|
|
architecture: str,
|
|
quantization: dict[str, Any],
|
|
reasoning_parser: str,
|
|
) -> None:
|
|
"""Reviewed Apple-local architectures should resolve to the MLX adapter."""
|
|
decision = llm_compatibility_module.resolve_compatibility(
|
|
{
|
|
"architectures": [architecture],
|
|
"dtype": "bfloat16",
|
|
"text_config": {"max_position_embeddings": 65536},
|
|
"quantization": quantization,
|
|
},
|
|
artifact_bytes=2 * 1024**3,
|
|
execution_target="local_apple",
|
|
)
|
|
|
|
assert decision.backend == "mlx_vlm"
|
|
assert decision.reasoning_parser == reasoning_parser
|
|
assert decision.runtime_requirements == ("mlx-vlm==0.6.15",)
|
|
assert decision.estimated_vram_gb < 5
|
|
|
|
|
|
def test_apple_local_compatibility_rejects_cuda_quantization(
|
|
llm_compatibility_module: Any,
|
|
) -> None:
|
|
"""Apple resolution should fail before downloading CUDA-specific weights."""
|
|
with pytest.raises(ValueError, match="not an MLX checkpoint format"):
|
|
llm_compatibility_module.resolve_compatibility(
|
|
{
|
|
"architectures": ["Qwen3_5ForConditionalGeneration"],
|
|
"text_config": {"max_position_embeddings": 32768},
|
|
"quantization_config": {"quant_method": "fp8"},
|
|
},
|
|
artifact_bytes=1024,
|
|
execution_target="local_apple",
|
|
)
|
|
|
|
|
|
def test_curated_profile_adapts_to_apple_without_mutating_modal_profile(
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""One saved curated id should resolve independently for both targets."""
|
|
modal_profile = llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
|
|
local_profile = llm_profiles_module.profile_for_execution_target(
|
|
modal_profile,
|
|
"local_apple",
|
|
)
|
|
|
|
assert modal_profile.backend == "transformers"
|
|
assert modal_profile.execution_target == "modal"
|
|
assert local_profile.profile_id == modal_profile.profile_id
|
|
assert local_profile.repository == modal_profile.repository
|
|
assert local_profile.backend == "mlx_vlm"
|
|
assert local_profile.execution_target == "local_apple"
|
|
|
|
|
|
class _FakeReasoningTokenizer:
|
|
"""Decode deterministic IDs while modelling Qwen's special think tokens."""
|
|
|
|
_vocabulary = {"<think>": 10, "</think>": 11}
|
|
_text = {1: "consider", 2: " carefully", 3: "final answer", 10: "", 11: ""}
|
|
|
|
def get_vocab(self) -> dict[str, int]:
|
|
"""Return exact reasoning boundary token IDs."""
|
|
return dict(self._vocabulary)
|
|
|
|
def decode(self, token_ids: Any, **kwargs: Any) -> str:
|
|
"""Decode content while respecting special-token cleanup."""
|
|
del kwargs
|
|
return "".join(self._text[token_id] for token_id in token_ids)
|
|
|
|
|
|
def test_qwen_reasoning_parser_separates_token_channels(
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""Qwen reasoning must never remain in the primary response string."""
|
|
parser = llm_reasoning_module.Qwen3ReasoningParser(_FakeReasoningTokenizer())
|
|
|
|
result = parser.extract(
|
|
"<think>consider carefully</think>final answer",
|
|
[10, 1, 2, 11, 11, 3],
|
|
)
|
|
|
|
assert result.response == "final answer"
|
|
assert result.reasoning == "consider carefully"
|
|
assert result.reasoning_tokens == 2
|
|
assert result.parser == "qwen3"
|
|
|
|
|
|
def test_reasoning_parser_uses_architecture_fallback_for_existing_profile(
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""Profiles generated before the parser field existed should remain usable."""
|
|
profile = SimpleNamespace(
|
|
architecture="Qwen3_5ForConditionalGeneration",
|
|
reasoning_parser="",
|
|
)
|
|
|
|
parser = llm_reasoning_module.create_reasoning_parser(
|
|
profile,
|
|
_FakeReasoningTokenizer(),
|
|
)
|
|
|
|
assert parser.parser_name == "qwen3"
|
|
|
|
|
|
def test_reasoning_chat_template_uses_qwen_hard_switch(
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""Reasoning-capable profiles should receive the exact per-request switch."""
|
|
qwen_profile = SimpleNamespace(
|
|
architecture="Qwen3_5ForConditionalGeneration",
|
|
reasoning_parser="qwen3",
|
|
)
|
|
ordinary_profile = SimpleNamespace(
|
|
architecture="SmolVLMForConditionalGeneration",
|
|
reasoning_parser="none",
|
|
)
|
|
|
|
assert llm_reasoning_module.reasoning_chat_template_kwargs(
|
|
qwen_profile,
|
|
True,
|
|
) == {"enable_thinking": True}
|
|
assert llm_reasoning_module.reasoning_chat_template_kwargs(
|
|
qwen_profile,
|
|
False,
|
|
) == {"enable_thinking": False}
|
|
assert (
|
|
llm_reasoning_module.reasoning_chat_template_kwargs(
|
|
ordinary_profile,
|
|
False,
|
|
)
|
|
== {}
|
|
)
|
|
|
|
|
|
def test_disabled_reasoning_parser_returns_only_direct_response(
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""Disabling reasoning must not classify a delimiter-free answer as thinking."""
|
|
configured_parser = llm_reasoning_module.Qwen3ReasoningParser(
|
|
_FakeReasoningTokenizer()
|
|
)
|
|
parser = llm_reasoning_module.reasoning_parser_for_request(
|
|
configured_parser,
|
|
False,
|
|
)
|
|
|
|
result = parser.extract(
|
|
"final answer",
|
|
[3],
|
|
native_reasoning="unexpected backend reasoning",
|
|
)
|
|
|
|
assert result.response == "final answer"
|
|
assert result.reasoning == ""
|
|
assert result.reasoning_tokens == 0
|
|
assert result.parser == "disabled"
|
|
|
|
|
|
def test_qwen_reasoning_parser_keeps_truncated_thinking_out_of_response(
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""A token-limit stop before </think> is reasoning-only, not a final answer."""
|
|
parser = llm_reasoning_module.Qwen3ReasoningParser(_FakeReasoningTokenizer())
|
|
|
|
result = parser.extract("consider carefully", [1, 2])
|
|
|
|
assert result.response == ""
|
|
assert result.reasoning == "consider carefully"
|
|
assert result.reasoning_tokens == 2
|
|
|
|
|
|
def test_reasoning_parser_prefers_engine_native_channel(
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""A future backend-native split should bypass delimiter parsing."""
|
|
parser = llm_reasoning_module.Qwen3ReasoningParser(_FakeReasoningTokenizer())
|
|
|
|
result = parser.extract(
|
|
"final answer",
|
|
[3],
|
|
native_reasoning="engine reasoning",
|
|
)
|
|
|
|
assert result.response == "final answer"
|
|
assert result.reasoning == "engine reasoning"
|
|
assert result.parser == "native"
|
|
|
|
|
|
def test_generated_profile_requires_matching_content_digest(
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""A generated manifest must not be mutable under a stable profile identifier."""
|
|
digest = "a" * 64
|
|
with pytest.raises(ValueError, match="does not match its content digest"):
|
|
llm_profiles_module.LLMModelProfile.from_mapping(
|
|
{
|
|
"id": "hf-" + "b" * 64,
|
|
"repository": "owner/model",
|
|
"revision": "1" * 40,
|
|
"dtype": "bfloat16",
|
|
"modalities": ["text"],
|
|
"estimated_vram_gb": 10,
|
|
"max_context_tokens": 1024,
|
|
"max_images": 1,
|
|
"max_video_frames": 1,
|
|
"max_file_bytes": 1,
|
|
"max_file_characters": 1,
|
|
"schema_version": 2,
|
|
"source": "generated",
|
|
"profile_digest": digest,
|
|
"backend": "transformers",
|
|
}
|
|
)
|
|
|
|
|
|
def test_cpu_resolver_pins_and_persists_generated_profile(
|
|
llm_resolver_module: Any,
|
|
llm_profiles_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""One model ID should become a stable manifest without downloading weights."""
|
|
revision = "9" * 40
|
|
config_path = tmp_path / "downloaded-config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"architectures": ["Qwen3_5ForConditionalGeneration"],
|
|
"dtype": "bfloat16",
|
|
"text_config": {"max_position_embeddings": 262144},
|
|
"quantization_config": {
|
|
"quant_method": "modelopt",
|
|
"quant_algo": "NVFP4",
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
model_info = {
|
|
"id": "owner/model",
|
|
"sha": revision,
|
|
"siblings": [
|
|
{"rfilename": "config.json", "size": 1000},
|
|
{"rfilename": "model-00001.safetensors", "size": 20 * 1024**3},
|
|
{"rfilename": "model-00002.safetensors", "size": 10 * 1024**3},
|
|
],
|
|
"securityStatus": {"scansDone": True, "filesWithIssues": []},
|
|
}
|
|
|
|
class FakeApi:
|
|
"""Return immutable test metadata from model_info."""
|
|
|
|
def model_info(self, repo_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
"""Validate the metadata-only resolver request."""
|
|
assert repo_id == "owner/model"
|
|
assert kwargs["revision"] == "release"
|
|
assert kwargs["files_metadata"] is True
|
|
return model_info
|
|
|
|
download_calls: list[dict[str, Any]] = []
|
|
|
|
def fake_hf_hub_download(**kwargs: Any) -> str:
|
|
"""Return only the config file during resolution."""
|
|
download_calls.append(kwargs)
|
|
return str(config_path)
|
|
|
|
first = llm_resolver_module.resolve_model_profile(
|
|
"owner/model@release",
|
|
tmp_path,
|
|
api=FakeApi(),
|
|
hf_hub_download=fake_hf_hub_download,
|
|
)
|
|
second = llm_resolver_module.resolve_model_profile(
|
|
"owner/model@release",
|
|
tmp_path,
|
|
api=FakeApi(),
|
|
hf_hub_download=fake_hf_hub_download,
|
|
)
|
|
|
|
assert first.profile.profile_id == second.profile.profile_id
|
|
assert first.profile.revision == revision
|
|
assert first.profile.backend == "vllm"
|
|
assert first.profile.reasoning_parser == "qwen3"
|
|
assert first.profile.quantization_method == "modelopt_fp4"
|
|
assert first.profile.artifact_bytes == 30 * 1024**3
|
|
assert first.manifest_created is True
|
|
assert second.manifest_created is False
|
|
assert len(download_calls) == 2
|
|
loaded = llm_profiles_module.get_llm_profile(
|
|
first.profile.profile_id,
|
|
storage_root=tmp_path,
|
|
)
|
|
assert loaded == first.profile
|
|
|
|
|
|
def test_local_resolver_creates_target_specific_mlx_profile(
|
|
llm_resolver_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The same user-facing model reference should get an Apple-local manifest."""
|
|
config_path = tmp_path / "mlx-config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"architectures": ["Qwen3_5ForConditionalGeneration"],
|
|
"dtype": "bfloat16",
|
|
"text_config": {"max_position_embeddings": 65536},
|
|
"quantization": {
|
|
"bits": 4,
|
|
"group_size": 64,
|
|
"mode": "affine",
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
class FakeApi:
|
|
"""Return one immutable MLX-format repository."""
|
|
|
|
def model_info(self, repo_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
"""Return compatible metadata without downloading model weights."""
|
|
del repo_id, kwargs
|
|
return {
|
|
"sha": "7" * 40,
|
|
"siblings": [
|
|
{"rfilename": "config.json", "size": 1000},
|
|
{"rfilename": "model.safetensors", "size": 2 * 1024**3},
|
|
],
|
|
"securityStatus": {"scansDone": True, "filesWithIssues": []},
|
|
}
|
|
|
|
result = llm_resolver_module.resolve_model_profile(
|
|
"mlx-community/Qwen3.5-2B-4bit",
|
|
tmp_path,
|
|
api=FakeApi(),
|
|
hf_hub_download=lambda **kwargs: str(config_path),
|
|
execution_target="local_apple",
|
|
)
|
|
|
|
assert result.profile.backend == "mlx_vlm"
|
|
assert result.profile.execution_target == "local_apple"
|
|
assert result.profile.quantization_method == "mlx_affine_4bit"
|
|
assert result.profile.runtime_requirements == ("mlx-vlm==0.6.15",)
|
|
|
|
|
|
def test_cpu_resolver_rejects_unknown_architecture_before_weights(
|
|
llm_resolver_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Compatibility errors should happen after config-only inspection."""
|
|
config_path = tmp_path / "config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"architectures": ["UnreviewedForConditionalGeneration"],
|
|
"max_position_embeddings": 4096,
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
class FakeApi:
|
|
"""Return a safe but unsupported test repository."""
|
|
|
|
def model_info(self, repo_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
"""Return metadata without performing network access."""
|
|
del repo_id, kwargs
|
|
return {
|
|
"sha": "8" * 40,
|
|
"siblings": [
|
|
{"rfilename": "config.json", "size": 1},
|
|
{"rfilename": "model.safetensors", "size": 1024},
|
|
],
|
|
"securityStatus": {"scansDone": True},
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="not supported"):
|
|
llm_resolver_module.resolve_model_profile(
|
|
"owner/unknown",
|
|
tmp_path,
|
|
api=FakeApi(),
|
|
hf_hub_download=lambda **kwargs: str(config_path),
|
|
)
|
|
assert not (tmp_path / "llm_models").exists()
|
|
|
|
|
|
def test_cpu_resolver_wraps_gated_config_download_error(
|
|
llm_resolver_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A gated config failure should cross Modal as a plain actionable ValueError."""
|
|
|
|
class FakeApi:
|
|
"""Return metadata visible without gated file access."""
|
|
|
|
def model_info(self, repo_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
"""Return one otherwise compatible repository."""
|
|
del repo_id, kwargs
|
|
return {
|
|
"sha": "8" * 40,
|
|
"siblings": [
|
|
{"rfilename": "config.json", "size": 1},
|
|
{"rfilename": "model.safetensors", "size": 1024},
|
|
],
|
|
"securityStatus": {"scansDone": True},
|
|
}
|
|
|
|
def denied_download(**kwargs: Any) -> str:
|
|
"""Simulate gated file access after public metadata resolution."""
|
|
del kwargs
|
|
raise OSError("401 Unauthorized")
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Unable to download config.json.*HF_TOKEN",
|
|
):
|
|
llm_resolver_module.resolve_model_profile(
|
|
"owner/gated",
|
|
tmp_path,
|
|
api=FakeApi(),
|
|
hf_hub_download=denied_download,
|
|
)
|
|
|
|
|
|
def test_local_resolver_reports_local_hugging_face_token_location(
|
|
llm_resolver_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Gated local models should not direct users to a Modal secret."""
|
|
|
|
class DeniedApi:
|
|
"""Reject model metadata as a gated repository would."""
|
|
|
|
def model_info(self, repo_id: str, **kwargs: Any) -> Any:
|
|
"""Raise a deterministic authorization error."""
|
|
del repo_id, kwargs
|
|
raise OSError("401 Unauthorized")
|
|
|
|
with pytest.raises(ValueError, match="set HF_TOKEN in the local ComfyUI"):
|
|
llm_resolver_module.resolve_model_profile(
|
|
"owner/gated",
|
|
tmp_path,
|
|
api=DeniedApi(),
|
|
hf_hub_download=lambda **kwargs: "unused",
|
|
execution_target="local_apple",
|
|
)
|
|
|
|
|
|
def test_cpu_stager_writes_completion_marker_and_reuses_snapshot(
|
|
llm_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A completed immutable snapshot should not download twice."""
|
|
monkeypatch.setenv("HF_TOKEN", "test-hugging-face-token")
|
|
calls: list[dict[str, Any]] = []
|
|
progress: list[Any] = []
|
|
|
|
def fake_snapshot_download(**kwargs: Any) -> str:
|
|
"""Materialize the minimum expected Hugging Face snapshot."""
|
|
calls.append(kwargs)
|
|
snapshot_path = Path(kwargs["local_dir"])
|
|
snapshot_path.mkdir(parents=True, exist_ok=True)
|
|
(snapshot_path / "config.json").write_text("{}", encoding="utf-8")
|
|
progress_bar = kwargs["tqdm_class"](
|
|
total=2,
|
|
unit="files",
|
|
file=StringIO(),
|
|
)
|
|
progress_bar.update(1)
|
|
progress_bar.update(1)
|
|
progress_bar.close()
|
|
return str(snapshot_path)
|
|
|
|
first = llm_staging_module.stage_model_profile(
|
|
"smolvlm2-2.2b-instruct",
|
|
tmp_path,
|
|
snapshot_download=fake_snapshot_download,
|
|
progress_callback=progress.append,
|
|
)
|
|
second = llm_staging_module.stage_model_profile(
|
|
"smolvlm2-2.2b-instruct",
|
|
tmp_path,
|
|
snapshot_download=fake_snapshot_download,
|
|
)
|
|
|
|
assert first.downloaded is True
|
|
assert second.downloaded is False
|
|
assert len(calls) == 1
|
|
assert calls[0]["revision"] == "482adb537c021c86670beed01cd58990d01e72e4"
|
|
assert calls[0]["token"] == "test-hugging-face-token"
|
|
assert "*.safetensors" in calls[0]["allow_patterns"]
|
|
assert "*.bin" not in calls[0]["allow_patterns"]
|
|
assert calls[0]["tqdm_class"].__name__ == "SnapshotProgressTqdm"
|
|
assert [event.value for event in progress if event.stage == "download"] == [
|
|
0.0,
|
|
1.0,
|
|
2.0,
|
|
]
|
|
assert [event.stage for event in progress] == [
|
|
"snapshot_check",
|
|
"disk_check",
|
|
"download_preparing",
|
|
"download",
|
|
"download",
|
|
"download",
|
|
"staged",
|
|
]
|
|
assert {event.model_reference for event in progress} == {
|
|
"smolvlm2-2.2b-instruct"
|
|
}
|
|
assert llm_staging_module.is_model_snapshot_staged(
|
|
tmp_path,
|
|
llm_staging_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
)
|
|
|
|
|
|
def test_curated_huihui_gguf_profile_pins_bounded_multimodal_artifacts(
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""The lambda profile should name one bounded GGUF rather than the whole repo."""
|
|
profile = llm_profiles_module.get_llm_profile(
|
|
"huihui-qwen3.8-27b-abliterated-q2-k-gguf"
|
|
)
|
|
|
|
assert profile.backend == "llama_cpp_server"
|
|
assert profile.repository == "huihui-ai/Huihui-Qwen3.8-27B-abliterated-GGUF"
|
|
assert profile.estimated_vram_gb == 16.0
|
|
assert profile.modalities == frozenset({"text", "image", "file"})
|
|
assert profile.backend_option("model_filename") == (
|
|
"Huihui-Qwen3.8-27B-abliterated-Q2_K.gguf"
|
|
)
|
|
assert profile.backend_option("mmproj_filename") == "mmproj-model-bf16.gguf"
|
|
|
|
|
|
def test_gguf_stager_downloads_only_selected_model_and_pinned_tokenizer(
|
|
llm_staging_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""GGUF staging must not fetch every quant or the tokenizer repo's weights."""
|
|
profile = llm_staging_module.get_llm_profile(
|
|
"huihui-qwen3.8-27b-abliterated-q2-k-gguf"
|
|
)
|
|
calls: list[dict[str, Any]] = []
|
|
|
|
def fake_snapshot_download(**kwargs: Any) -> str:
|
|
"""Materialize the selected GGUF and tokenizer sentinel."""
|
|
calls.append(kwargs)
|
|
snapshot_path = Path(kwargs["local_dir"])
|
|
snapshot_path.mkdir(parents=True, exist_ok=True)
|
|
if kwargs["repo_id"] == profile.repository:
|
|
(snapshot_path / profile.backend_option("model_filename")).write_bytes(
|
|
b"gguf"
|
|
)
|
|
(snapshot_path / profile.backend_option("mmproj_filename")).write_bytes(
|
|
b"mmproj"
|
|
)
|
|
else:
|
|
(snapshot_path / "tokenizer_config.json").write_text("{}", encoding="utf-8")
|
|
return str(snapshot_path)
|
|
|
|
result = llm_staging_module.stage_model_profile(
|
|
profile.profile_id,
|
|
tmp_path,
|
|
profile=profile,
|
|
snapshot_download=fake_snapshot_download,
|
|
)
|
|
|
|
assert result.downloaded is True
|
|
assert len(calls) == 2
|
|
assert calls[0]["allow_patterns"][-2:] == (
|
|
profile.backend_option("model_filename"),
|
|
profile.backend_option("mmproj_filename"),
|
|
)
|
|
assert "*.gguf" not in calls[0]["allow_patterns"]
|
|
assert "*.safetensors" not in calls[1]["allow_patterns"]
|
|
assert calls[1]["repo_id"] == profile.backend_option("tokenizer_repository")
|
|
assert calls[1]["revision"] == profile.backend_option("tokenizer_revision")
|
|
assert llm_staging_module.is_model_snapshot_staged(tmp_path, profile)
|
|
|
|
|
|
def test_provider_neutral_stager_resolves_and_stages_model_reference(
|
|
llm_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Modal and SSH staging should share one immutable resolution implementation."""
|
|
profile = SimpleNamespace(
|
|
profile_id="hf-" + "c" * 64,
|
|
repository="owner/model",
|
|
revision="9" * 40,
|
|
backend="vllm",
|
|
quantization_method="compressed-tensors",
|
|
artifact_bytes=12 * 1024**3,
|
|
)
|
|
staged = llm_staging_module.StagedModelSnapshot(
|
|
profile_id=profile.profile_id,
|
|
repository=profile.repository,
|
|
revision=profile.revision,
|
|
path=str(tmp_path / "snapshot"),
|
|
downloaded=True,
|
|
elapsed_seconds=3.0,
|
|
)
|
|
progress: list[Any] = []
|
|
monkeypatch.setattr(
|
|
llm_staging_module,
|
|
"_resolve_profile_for_staging",
|
|
lambda _reference, _root: (profile, "/storage/manifest.json", True, True),
|
|
)
|
|
monkeypatch.setattr(
|
|
llm_staging_module,
|
|
"stage_model_profile",
|
|
lambda *_args, **_kwargs: staged,
|
|
)
|
|
|
|
results = llm_staging_module.resolve_and_stage_model_references(
|
|
["owner/model"],
|
|
tmp_path,
|
|
progress_callback=progress.append,
|
|
)
|
|
|
|
assert results[0].requested_reference == "owner/model"
|
|
assert results[0].profile_id == profile.profile_id
|
|
assert results[0].manifest_created is True
|
|
assert results[0].downloaded is True
|
|
assert progress[0].stage == "metadata"
|
|
assert progress[0].message == "Inspecting Hugging Face metadata for owner/model"
|
|
assert progress[0].model_reference == "owner/model"
|
|
|
|
|
|
def test_provider_neutral_stager_uses_planner_resolved_profile(
|
|
llm_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Remote staging must not repeat Hugging Face inspection done by the planner."""
|
|
digest = "c" * 64
|
|
raw_profile = {
|
|
"id": f"hf-{digest}",
|
|
"display_name": "Owner Model",
|
|
"repository": "owner/model",
|
|
"revision": "9" * 40,
|
|
"dtype": "bfloat16",
|
|
"modalities": ["text"],
|
|
"estimated_vram_gb": 12,
|
|
"max_context_tokens": 4096,
|
|
"max_images": 1,
|
|
"max_video_frames": 1,
|
|
"max_file_bytes": 1024,
|
|
"max_file_characters": 1024,
|
|
"allow_mixed_image_video": False,
|
|
"trust_remote_code": False,
|
|
"schema_version": 2,
|
|
"source": "generated",
|
|
"profile_digest": digest,
|
|
"backend": "transformers",
|
|
"artifact_bytes": 1024,
|
|
}
|
|
staged = llm_staging_module.StagedModelSnapshot(
|
|
profile_id=raw_profile["id"],
|
|
repository="owner/model",
|
|
revision="9" * 40,
|
|
path=str(tmp_path / "snapshot"),
|
|
downloaded=False,
|
|
elapsed_seconds=0.0,
|
|
)
|
|
monkeypatch.setattr(
|
|
llm_staging_module,
|
|
"stage_model_profile",
|
|
lambda *_args, **_kwargs: staged,
|
|
)
|
|
progress: list[Any] = []
|
|
|
|
results = llm_staging_module.resolve_and_stage_model_references(
|
|
["owner/model"],
|
|
tmp_path,
|
|
resolved_profiles={
|
|
"owner/model": {
|
|
"profile": raw_profile,
|
|
"security_scan_complete": False,
|
|
}
|
|
},
|
|
progress_callback=progress.append,
|
|
owner_id="vast:invocation:owner",
|
|
)
|
|
|
|
assert results[0].profile_id == raw_profile["id"]
|
|
assert results[0].security_scan_complete is False
|
|
assert results[0].manifest_created is True
|
|
assert progress[0].stage == "resolved_metadata"
|
|
assert "planner-resolved" in progress[0].message
|
|
|
|
|
|
def test_snapshot_lease_reclaims_dead_local_owner(
|
|
llm_staging_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A dead process record should never force a two-hour stale-lock wait."""
|
|
snapshot_path = tmp_path / "llm_models" / "profile" / ("1" * 40)
|
|
snapshot_path.parent.mkdir(parents=True)
|
|
lease_path = snapshot_path.parent / f".{snapshot_path.name}.download.lock"
|
|
lease_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"owner_id": "orphan",
|
|
"host_id": llm_staging_module.socket.gethostname(),
|
|
"pid": 2_000_000_000,
|
|
"process_start": "missing",
|
|
"token": "old-token",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with llm_staging_module._snapshot_lease(
|
|
snapshot_path,
|
|
model_label="test model",
|
|
owner_id="replacement",
|
|
):
|
|
owner = json.loads(lease_path.read_text(encoding="utf-8"))
|
|
assert owner["owner_id"] == "replacement"
|
|
|
|
assert not lease_path.exists()
|
|
|
|
|
|
def test_snapshot_lease_recognizes_live_owner_without_procfs(
|
|
llm_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Portable PID probing should keep a live macOS staging owner valid."""
|
|
monkeypatch.setattr(
|
|
llm_staging_module,
|
|
"_process_start_identity",
|
|
lambda _pid: None,
|
|
)
|
|
|
|
alive = llm_staging_module._local_lease_owner_is_alive(
|
|
{
|
|
"host_id": llm_staging_module.socket.gethostname(),
|
|
"pid": llm_staging_module.os.getpid(),
|
|
"process_start": None,
|
|
}
|
|
)
|
|
|
|
assert alive is True
|
|
|
|
|
|
def test_snapshot_lease_reclaims_missing_foreign_heartbeat(
|
|
llm_staging_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A vanished prior container should not leave a structured lease for hours."""
|
|
snapshot_path = tmp_path / "llm_models" / "profile" / ("2" * 40)
|
|
snapshot_path.parent.mkdir(parents=True)
|
|
lease_path = snapshot_path.parent / f".{snapshot_path.name}.download.lock"
|
|
lease_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"owner_id": "old-container",
|
|
"host_id": "different-container",
|
|
"pid": 123,
|
|
"process_start": "456",
|
|
"token": "old-token",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
stale_time = (
|
|
llm_staging_module.time.time()
|
|
- llm_staging_module._DEFAULT_LEASE_HEARTBEAT_STALE_SECONDS
|
|
- 1
|
|
)
|
|
llm_staging_module.os.utime(lease_path, (stale_time, stale_time))
|
|
|
|
with llm_staging_module._snapshot_lease(
|
|
snapshot_path,
|
|
model_label="test model",
|
|
owner_id="replacement",
|
|
):
|
|
owner = json.loads(lease_path.read_text(encoding="utf-8"))
|
|
assert owner["owner_id"] == "replacement"
|
|
|
|
assert not lease_path.exists()
|
|
|
|
|
|
def test_snapshot_disk_preflight_rejects_insufficient_capacity(
|
|
llm_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Staging should fail before download when artifacts plus reserve do not fit."""
|
|
profile = llm_staging_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
monkeypatch.setattr(
|
|
llm_staging_module.shutil,
|
|
"disk_usage",
|
|
lambda _path: SimpleNamespace(free=profile.artifact_bytes),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="Insufficient disk space"):
|
|
llm_staging_module._preflight_snapshot_capacity(
|
|
tmp_path / "snapshot",
|
|
profile,
|
|
)
|
|
|
|
|
|
def test_direct_stager_payload_encodes_planner_profiles(
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""SSH and Vast CLI staging should receive the validated planner envelope."""
|
|
profile = llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
entry = {
|
|
"profile": profile.to_mapping(),
|
|
"security_scan_complete": True,
|
|
}
|
|
|
|
encoded = llm_profiles_module.encoded_resolved_llm_profile_payloads(
|
|
{"resolved_llm_profiles": {profile.profile_id: entry}},
|
|
[profile.profile_id],
|
|
)
|
|
|
|
assert encoded is not None
|
|
decoded = json.loads(base64.urlsafe_b64decode(encoded).decode("utf-8"))
|
|
assert decoded == {profile.profile_id: entry}
|
|
|
|
|
|
def test_weight_snapshot_is_shared_across_runtime_profiles(
|
|
llm_staging_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Runtime tuning changes must not duplicate one repository revision's weights."""
|
|
calls: list[dict[str, Any]] = []
|
|
base_profile = llm_staging_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
throughput_profile = replace(
|
|
base_profile,
|
|
profile_id="smolvlm-throughput-test",
|
|
backend_options=(("enforce_eager", False),),
|
|
)
|
|
|
|
def fake_snapshot_download(**kwargs: Any) -> str:
|
|
"""Materialize one minimal canonical snapshot."""
|
|
calls.append(kwargs)
|
|
snapshot_path = Path(kwargs["local_dir"])
|
|
snapshot_path.mkdir(parents=True, exist_ok=True)
|
|
(snapshot_path / "config.json").write_text("{}", encoding="utf-8")
|
|
return str(snapshot_path)
|
|
|
|
eager = llm_staging_module.stage_model_profile(
|
|
base_profile.profile_id,
|
|
tmp_path,
|
|
profile=base_profile,
|
|
snapshot_download=fake_snapshot_download,
|
|
)
|
|
throughput = llm_staging_module.stage_model_profile(
|
|
throughput_profile.profile_id,
|
|
tmp_path,
|
|
profile=throughput_profile,
|
|
snapshot_download=fake_snapshot_download,
|
|
)
|
|
|
|
assert eager.path == throughput.path
|
|
assert eager.downloaded is True
|
|
assert throughput.downloaded is False
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_stager_reuses_legacy_profile_keyed_weight_snapshot_in_place(
|
|
llm_staging_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Existing profile-keyed weights should remain usable by older deployments."""
|
|
profile = llm_staging_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
legacy_path = tmp_path / "llm_models" / profile.profile_id / profile.revision
|
|
legacy_path.mkdir(parents=True)
|
|
(legacy_path / "config.json").write_text("{}", encoding="utf-8")
|
|
(legacy_path / ".comfy-modal-llm-complete.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"profile_id": profile.profile_id,
|
|
"repository": profile.repository,
|
|
"revision": profile.revision,
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = llm_staging_module.stage_model_profile(
|
|
profile.profile_id,
|
|
tmp_path,
|
|
profile=profile,
|
|
snapshot_download=lambda **kwargs: pytest.fail(
|
|
f"unexpected snapshot download: {kwargs}"
|
|
),
|
|
)
|
|
|
|
assert result.downloaded is False
|
|
assert Path(result.path) == legacy_path
|
|
assert legacy_path.is_dir()
|
|
|
|
|
|
def test_multimodal_preparation_samples_video_and_bounds_files(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""Native media and files should normalize without transport encoding."""
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
allow_mixed_image_video=True,
|
|
)
|
|
images = torch.zeros((2, 4, 5, 3), dtype=torch.float32)
|
|
video_frames = torch.stack(
|
|
[torch.full((4, 5, 3), index / 9.0) for index in range(10)]
|
|
)
|
|
|
|
class FakeVideo:
|
|
"""Expose the native ComfyUI video component contract."""
|
|
|
|
def get_components(self) -> Any:
|
|
"""Return ten frames at two frames per second."""
|
|
return SimpleNamespace(images=video_frames, frame_rate=Fraction(2, 1))
|
|
|
|
prepared = modal_llm_runtime_module.prepare_llm_inputs(
|
|
prompt="Compare the media.",
|
|
system_prompt="Be concise.",
|
|
images=images,
|
|
video=FakeVideo(),
|
|
files=[_text_file("notes.txt", "important context")],
|
|
video_frames=3,
|
|
profile=profile,
|
|
)
|
|
|
|
assert len(prepared.images) == 2
|
|
assert prepared.images[0].size == (5, 4)
|
|
assert prepared.video.timestamps_seconds == (0.0, 2.0, 4.5)
|
|
assert "notes.txt" in prepared.prompt
|
|
assert "important context" in prepared.prompt
|
|
assert prepared.file_count == 1
|
|
|
|
|
|
def test_profile_specific_mixed_media_restriction(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""The initial SmolVLM profile should fail clearly for unsupported mixed media."""
|
|
profile = llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
|
|
class FakeVideo:
|
|
"""Expose a single native video frame."""
|
|
|
|
def get_components(self) -> Any:
|
|
"""Return one frame at one frame per second."""
|
|
return SimpleNamespace(
|
|
images=torch.zeros((1, 2, 2, 3)),
|
|
frame_rate=Fraction(1, 1),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="images or video"):
|
|
modal_llm_runtime_module.prepare_llm_inputs(
|
|
prompt="Describe.",
|
|
system_prompt="",
|
|
images=torch.zeros((1, 2, 2, 3)),
|
|
video=FakeVideo(),
|
|
files=None,
|
|
video_frames=1,
|
|
profile=profile,
|
|
)
|
|
|
|
|
|
def test_pdf_without_extractable_text_is_rejected(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""Scanned or blank PDFs should fail clearly instead of adding empty context."""
|
|
from pypdf import PdfWriter
|
|
|
|
pdf_buffer = BytesIO()
|
|
writer = PdfWriter()
|
|
writer.add_blank_page(width=72, height=72)
|
|
writer.write(pdf_buffer)
|
|
encoded = base64.b64encode(pdf_buffer.getvalue()).decode("ascii")
|
|
with pytest.raises(ValueError, match="no extractable text"):
|
|
modal_llm_runtime_module.extract_file_context(
|
|
[
|
|
{
|
|
"filename": "blank.pdf",
|
|
"file_data": f"data:application/pdf;base64,{encoded}",
|
|
}
|
|
],
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
)
|
|
|
|
|
|
def test_transformers_stopping_criteria_propagates_cancellation(
|
|
modal_llm_runtime_module: Any,
|
|
) -> None:
|
|
"""Per-token criteria should surface the same interruption raised by ComfyUI."""
|
|
calls: list[int] = []
|
|
|
|
def cancel_on_second_token(progress: Any) -> None:
|
|
"""Raise a cancellation marker on the second generation step."""
|
|
calls.append(int(progress.value))
|
|
if progress.value == 2:
|
|
raise InterruptedError("cancelled")
|
|
|
|
criterion = modal_llm_runtime_module._stopping_criteria(cancel_on_second_token)[0]
|
|
|
|
assert criterion(None, None) is False
|
|
with pytest.raises(InterruptedError, match="cancelled"):
|
|
criterion(None, None)
|
|
assert calls == [1, 2]
|
|
|
|
|
|
def test_transformers_processor_does_not_resample_predecoded_video(
|
|
modal_llm_runtime_module: Any,
|
|
) -> None:
|
|
"""Native ComfyUI frames should bypass the processor's metadata-based sampler."""
|
|
calls: list[tuple[list[dict[str, Any]], dict[str, Any]]] = []
|
|
|
|
class FakeProcessor:
|
|
"""Record the chat-template call without importing a real model processor."""
|
|
|
|
def apply_chat_template(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
**kwargs: Any,
|
|
) -> str:
|
|
"""Capture processor options and return a tokenization sentinel."""
|
|
calls.append((messages, kwargs))
|
|
return "tokenized"
|
|
|
|
messages = [{"role": "user", "content": [{"type": "video", "video": [object()]}]}]
|
|
|
|
result = modal_llm_runtime_module._apply_multimodal_chat_template(
|
|
FakeProcessor(),
|
|
messages,
|
|
has_predecoded_video=True,
|
|
)
|
|
|
|
assert result == "tokenized"
|
|
assert calls[0][1]["do_sample_frames"] is False
|
|
assert "processor_kwargs" not in calls[0][1]
|
|
|
|
|
|
def test_vllm_backend_uses_explicit_kv_budget_and_local_multimodal_data(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The vLLM adapter should not reserve memory from a whole-GPU fraction."""
|
|
import transformers
|
|
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
profile_id="qwen-vllm-test",
|
|
backend="vllm",
|
|
architecture="Qwen3_5ForConditionalGeneration",
|
|
reasoning_parser="qwen3",
|
|
quantization_method="fp8",
|
|
backend_options=(
|
|
("enforce_eager", True),
|
|
("kv_cache_memory_bytes", 12 * 1024**3),
|
|
("max_model_len", 32768),
|
|
("quantization", "fp8"),
|
|
),
|
|
)
|
|
observed: dict[str, Any] = {}
|
|
|
|
class FakeProcessor:
|
|
"""Render a deterministic prompt from processor-native messages."""
|
|
|
|
tokenizer = _FakeReasoningTokenizer()
|
|
|
|
def apply_chat_template(self, messages: Any, **kwargs: Any) -> str:
|
|
"""Record that tokenization stays inside vLLM."""
|
|
observed["messages"] = messages
|
|
observed["chat_kwargs"] = kwargs
|
|
return "rendered prompt"
|
|
|
|
class FakeAsyncEngineArgs:
|
|
"""Capture explicit asynchronous engine construction arguments."""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
"""Retain the co-residency policy for assertions."""
|
|
self.kwargs = kwargs
|
|
|
|
class FakeAsyncLLM:
|
|
"""Stream deterministic cumulative vLLM request outputs."""
|
|
|
|
@classmethod
|
|
def from_engine_args(cls, engine_args: FakeAsyncEngineArgs) -> Any:
|
|
"""Capture engine arguments and return one resident engine."""
|
|
observed["llm_kwargs"] = engine_args.kwargs
|
|
return cls()
|
|
|
|
async def generate(self, prompt: Any, **kwargs: Any) -> Any:
|
|
"""Yield two cumulative token updates from one request."""
|
|
observed["prompts"] = prompt
|
|
observed["generate_kwargs"] = kwargs
|
|
yield SimpleNamespace(
|
|
prompt_token_ids=[1, 2, 3],
|
|
outputs=[SimpleNamespace(text="<think>consider", token_ids=[10, 1])],
|
|
)
|
|
yield SimpleNamespace(
|
|
prompt_token_ids=[1, 2, 3],
|
|
outputs=[
|
|
SimpleNamespace(
|
|
text="<think>consider carefully</think>final answer",
|
|
token_ids=[10, 1, 2, 11, 3],
|
|
)
|
|
],
|
|
)
|
|
|
|
async def abort(self, request_id: str) -> None:
|
|
"""Record unexpected request cancellation."""
|
|
observed["aborted"] = request_id
|
|
|
|
def shutdown(self) -> None:
|
|
"""Record deterministic engine cleanup."""
|
|
observed["shutdown"] = True
|
|
|
|
class FakeSamplingParams:
|
|
"""Capture backend-neutral sampling settings."""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
"""Retain settings for assertions."""
|
|
self.kwargs = kwargs
|
|
|
|
class FakeEngineDeadError(Exception):
|
|
"""Stand in for vLLM's package-private engine exception."""
|
|
|
|
monkeypatch.setattr(
|
|
transformers.AutoProcessor,
|
|
"from_pretrained",
|
|
lambda *args, **kwargs: FakeProcessor(),
|
|
)
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"vllm",
|
|
SimpleNamespace(
|
|
AsyncEngineArgs=FakeAsyncEngineArgs,
|
|
AsyncLLMEngine=FakeAsyncLLM,
|
|
SamplingParams=FakeSamplingParams,
|
|
__path__=[],
|
|
),
|
|
)
|
|
monkeypatch.setitem(sys.modules, "vllm.v1", SimpleNamespace(__path__=[]))
|
|
monkeypatch.setitem(sys.modules, "vllm.v1.engine", SimpleNamespace(__path__=[]))
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"vllm.v1.engine.exceptions",
|
|
SimpleNamespace(EngineDeadError=FakeEngineDeadError),
|
|
)
|
|
progress: list[Any] = []
|
|
backend = modal_llm_runtime_module._default_backend_factory(
|
|
profile,
|
|
tmp_path,
|
|
progress.append,
|
|
)
|
|
result = backend.generate(
|
|
modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
),
|
|
modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=16,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=4,
|
|
),
|
|
progress.append,
|
|
)
|
|
backend.unload()
|
|
|
|
assert observed["llm_kwargs"]["kv_cache_memory_bytes"] == 12 * 1024**3
|
|
assert "gpu_memory_utilization" not in observed["llm_kwargs"]
|
|
assert observed["llm_kwargs"]["quantization"] == "fp8"
|
|
assert observed["llm_kwargs"]["enforce_eager"] is True
|
|
assert observed["llm_kwargs"]["safetensors_load_strategy"] == "prefetch"
|
|
assert observed["llm_kwargs"]["attention_config"] == {
|
|
"backend": "TRITON_ATTN",
|
|
}
|
|
assert observed["prompts"] == {"prompt": "rendered prompt"}
|
|
assert observed["chat_kwargs"]["tokenize"] is False
|
|
assert observed["chat_kwargs"]["enable_thinking"] is True
|
|
assert [event.value for event in progress if event.stage == "generating"] == [2, 5]
|
|
assert progress[-1].tokens_per_second is not None
|
|
assert result.text == "final answer"
|
|
assert result.reasoning == "consider carefully"
|
|
assert result.reasoning_tokens == 2
|
|
assert result.input_tokens == 3
|
|
assert (
|
|
observed["generate_kwargs"]["sampling_params"].kwargs["skip_special_tokens"]
|
|
is False
|
|
)
|
|
assert observed["generate_kwargs"]["request_id"].startswith("modal-llm-")
|
|
assert observed["shutdown"] is True
|
|
|
|
|
|
def test_llama_cpp_backend_generates_with_curated_gguf_profile(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
llm_reasoning_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The GGUF adapter should preserve reasoning parsing and token telemetry."""
|
|
profile = llm_profiles_module.get_llm_profile(
|
|
"huihui-qwen3.8-27b-abliterated-q2-k-gguf"
|
|
)
|
|
model_path = tmp_path / profile.backend_option("model_filename")
|
|
model_path.write_bytes(b"gguf")
|
|
backend = object.__new__(modal_llm_runtime_module.LlamaCppServerBackend)
|
|
backend.profile = profile
|
|
backend.snapshot_path = tmp_path
|
|
backend.model_path = model_path
|
|
backend.processor = SimpleNamespace(
|
|
tokenizer=_FakeReasoningTokenizer(),
|
|
apply_chat_template=lambda *args, **kwargs: "rendered prompt",
|
|
)
|
|
backend.reasoning_parser = llm_reasoning_module.Qwen3ReasoningParser(
|
|
backend.processor.tokenizer
|
|
)
|
|
backend._completion = lambda payload, timeout_seconds: {
|
|
"content": "<think>consider</think>answer",
|
|
"tokens": [10, 1, 11, 3],
|
|
"tokens_evaluated": 7,
|
|
"timings": {"predicted_per_second": 12.5},
|
|
}
|
|
progress: list[Any] = []
|
|
|
|
result = backend.generate(
|
|
modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
),
|
|
modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=16,
|
|
temperature=0.2,
|
|
top_p=0.95,
|
|
seed=4,
|
|
),
|
|
progress.append,
|
|
)
|
|
|
|
assert result.text == "final answer"
|
|
assert result.reasoning == "consider"
|
|
assert result.input_tokens == 7
|
|
assert result.output_tokens == 4
|
|
assert result.tokens_per_second == 12.5
|
|
assert [event.stage for event in progress] == ["prefill", "generating"]
|
|
|
|
|
|
def test_llama_cpp_backend_adds_binary_directory_to_library_path(
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The private server should find shared libraries shipped beside its binary."""
|
|
backend = object.__new__(modal_llm_runtime_module.LlamaCppServerBackend)
|
|
backend.profile = SimpleNamespace(
|
|
profile_id="gguf-test",
|
|
max_context_tokens=8192,
|
|
backend_option=lambda _name, default=None: default,
|
|
)
|
|
backend.model_path = tmp_path / "model.gguf"
|
|
backend.mmproj_path = None
|
|
backend.port = 18080
|
|
backend._log_file = BytesIO()
|
|
observed: dict[str, Any] = {}
|
|
|
|
def popen(command: list[str], **kwargs: Any) -> SimpleNamespace:
|
|
"""Capture the subprocess environment for the private server."""
|
|
observed["command"] = command
|
|
observed["kwargs"] = kwargs
|
|
return SimpleNamespace()
|
|
|
|
monkeypatch.setenv("LD_LIBRARY_PATH", "/existing/lib")
|
|
monkeypatch.setattr(modal_llm_runtime_module.subprocess, "Popen", popen)
|
|
|
|
backend._start_server()
|
|
|
|
assert observed["command"][0] == "/app/llama-server"
|
|
assert observed["kwargs"]["env"]["LD_LIBRARY_PATH"] == (
|
|
"/app:/existing/lib"
|
|
)
|
|
|
|
|
|
def test_llama_cpp_backend_sends_images_through_chat_completion(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
llm_reasoning_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The curated projector should receive image data through the OAI route."""
|
|
profile = llm_profiles_module.get_llm_profile(
|
|
"huihui-qwen3.8-27b-abliterated-q2-k-gguf"
|
|
)
|
|
backend = object.__new__(modal_llm_runtime_module.LlamaCppServerBackend)
|
|
backend.profile = profile
|
|
backend.snapshot_path = tmp_path
|
|
backend.model_path = tmp_path / profile.backend_option("model_filename")
|
|
backend.mmproj_path = tmp_path / profile.backend_option("mmproj_filename")
|
|
backend.processor = SimpleNamespace(tokenizer=_FakeReasoningTokenizer())
|
|
backend.reasoning_parser = llm_reasoning_module.Qwen3ReasoningParser(
|
|
backend.processor.tokenizer
|
|
)
|
|
observed: dict[str, Any] = {}
|
|
|
|
def fake_chat_completion(
|
|
payload: dict[str, Any], timeout_seconds: float
|
|
) -> dict[str, Any]:
|
|
"""Capture one multimodal request and return OAI-style usage."""
|
|
observed["payload"] = payload
|
|
observed["timeout_seconds"] = timeout_seconds
|
|
return {
|
|
"choices": [{"message": {"content": "image answer"}}],
|
|
"usage": {"prompt_tokens": 101, "completion_tokens": 2},
|
|
"timings": {"predicted_per_second": 8.0},
|
|
}
|
|
|
|
backend._chat_completion = fake_chat_completion
|
|
result = backend.generate(
|
|
modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="describe",
|
|
system_prompt="be concise",
|
|
images=(Image.new("RGB", (4, 4), color=(255, 0, 0)),),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
),
|
|
modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=16,
|
|
temperature=0.2,
|
|
top_p=0.95,
|
|
seed=4,
|
|
enable_reasoning=False,
|
|
),
|
|
lambda _event: None,
|
|
)
|
|
|
|
messages = observed["payload"]["messages"]
|
|
assert messages[0] == {"role": "system", "content": "be concise"}
|
|
assert messages[1]["content"][0]["type"] == "image_url"
|
|
assert messages[1]["content"][0]["image_url"]["url"].startswith(
|
|
"data:image/png;base64,"
|
|
)
|
|
assert messages[1]["content"][1] == {"type": "text", "text": "describe"}
|
|
assert result.text == "image answer"
|
|
assert result.input_tokens == 101
|
|
assert result.output_tokens == 2
|
|
assert result.tokens_per_second == 8.0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("configured_mode", "expected_mode", "expected_enforce_eager"),
|
|
[
|
|
("auto", "eager", True),
|
|
("eager", "eager", True),
|
|
("throughput", "throughput", False),
|
|
],
|
|
)
|
|
def test_vllm_execution_setting_selects_initial_engine_policy(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
configured_mode: str,
|
|
expected_mode: str,
|
|
expected_enforce_eager: bool,
|
|
) -> None:
|
|
"""Pinned settings should win while auto starts with a low-latency eager engine."""
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="vllm",
|
|
backend_options=(("enforce_eager", True),),
|
|
)
|
|
monkeypatch.setenv("COMFY_MODAL_LLM_VLLM_EXECUTION_MODE", configured_mode)
|
|
|
|
mode, enforce_eager = modal_llm_runtime_module._vllm_execution_policy(profile)
|
|
|
|
assert mode == expected_mode
|
|
assert enforce_eager is expected_enforce_eager
|
|
|
|
|
|
@pytest.mark.parametrize("setting", ["eager", "throughput"])
|
|
def test_vllm_pinned_mode_never_auto_promotes(
|
|
modal_llm_runtime_module: Any,
|
|
setting: str,
|
|
) -> None:
|
|
"""Pinned controllers must ignore container workflow-count changes."""
|
|
controller = modal_llm_runtime_module.VLLMExecutionModeController(setting)
|
|
|
|
assert controller.observe("workflow-1") is False
|
|
assert controller.observe("workflow-2") is False
|
|
assert controller.effective_mode() == setting
|
|
assert controller.promoted is False
|
|
assert controller.observed_workflow_count == 0
|
|
|
|
|
|
def test_vllm_auto_mode_can_promote_before_first_llm_load(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
) -> None:
|
|
"""An image-only first workflow should still make a later LLM use throughput."""
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="vllm",
|
|
)
|
|
controller = modal_llm_runtime_module.VLLMExecutionModeController("auto")
|
|
|
|
controller.observe("image-workflow")
|
|
controller.observe("llm-workflow")
|
|
runtime_profile = modal_llm_runtime_module._profile_for_vllm_execution(
|
|
profile,
|
|
controller,
|
|
)
|
|
|
|
assert (
|
|
runtime_profile.backend_option(
|
|
modal_llm_runtime_module._VLLM_RUNTIME_MODE_OPTION
|
|
)
|
|
== "throughput"
|
|
)
|
|
|
|
|
|
def test_vllm_auto_mode_preserves_throughput_on_memory_recovery_worker(
|
|
modal_llm_runtime_module: Any,
|
|
) -> None:
|
|
"""A fresh recovery worker must not reset a promoted retry back to eager."""
|
|
controller = modal_llm_runtime_module.VLLMExecutionModeController("auto")
|
|
|
|
promoted = controller.force_throughput_after_memory_recovery("workflow-2")
|
|
|
|
assert promoted is True
|
|
assert controller.effective_mode() == "throughput"
|
|
assert controller.promoted is True
|
|
assert controller.observed_workflow_count == 2
|
|
assert controller.observe("workflow-2") is False
|
|
|
|
|
|
def test_vllm_auto_mode_promotes_on_second_distinct_workflow(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Repeat components stay eager, then the next workflow rebuilds for throughput."""
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="vllm",
|
|
)
|
|
controller = modal_llm_runtime_module.VLLMExecutionModeController("auto")
|
|
loaded_backends: list[Any] = []
|
|
|
|
class ModeBackend(_FakeBackend):
|
|
"""Expose the ephemeral vLLM mode attached by the resident manager."""
|
|
|
|
def __init__(self, runtime_profile: Any) -> None:
|
|
"""Record the mode selected for this fake engine construction."""
|
|
super().__init__(runtime_profile.profile_id)
|
|
self.mode = runtime_profile.backend_option(
|
|
modal_llm_runtime_module._VLLM_RUNTIME_MODE_OPTION
|
|
)
|
|
|
|
def runtime_metadata(self) -> dict[str, Any]:
|
|
"""Mirror the real vLLM backend's effective-mode telemetry."""
|
|
return {"vllm_execution_mode": self.mode}
|
|
|
|
def backend_factory(
|
|
runtime_profile: Any,
|
|
snapshot_path: Path,
|
|
progress_callback: Callable[[Any], None],
|
|
) -> ModeBackend:
|
|
"""Create a mode-aware fake backend for each engine rebuild."""
|
|
del snapshot_path, progress_callback
|
|
backend = ModeBackend(runtime_profile)
|
|
loaded_backends.append(backend)
|
|
return backend
|
|
|
|
comfy_memory_releases: list[int] = []
|
|
manager = modal_llm_runtime_module.ResidentLLMManager(
|
|
storage_root=tmp_path,
|
|
backend_factory=backend_factory,
|
|
memory_info=lambda: (200 * 1024**3, 256 * 1024**3),
|
|
empty_cache=lambda: None,
|
|
snapshot_ready=lambda storage_root, selected_profile: True,
|
|
comfy_memory_release=comfy_memory_releases.append,
|
|
vllm_mode_controller=controller,
|
|
)
|
|
prepared = modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
)
|
|
generation = modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=0,
|
|
)
|
|
progress: list[Any] = []
|
|
|
|
def infer(workflow_execution_id: str) -> Any:
|
|
"""Run one fake workflow through the shared resident manager."""
|
|
return manager.infer(
|
|
profile=profile,
|
|
prepared_inputs=prepared,
|
|
generation_settings=generation,
|
|
reserve_free_vram_gb=0,
|
|
keep_model_loaded=True,
|
|
progress_callback=progress.append,
|
|
workflow_execution_id=workflow_execution_id,
|
|
)
|
|
|
|
first = infer("workflow-1")
|
|
same_workflow = infer("workflow-1")
|
|
promoted = infer("workflow-2")
|
|
warm_throughput = infer("workflow-3")
|
|
|
|
assert first.metadata["vllm_execution_mode"] == "eager"
|
|
assert first.metadata["cache_hit"] is False
|
|
assert same_workflow.metadata["cache_hit"] is True
|
|
assert promoted.metadata["vllm_execution_mode"] == "throughput"
|
|
assert promoted.metadata["vllm_execution_setting"] == "auto"
|
|
assert promoted.metadata["vllm_auto_promoted"] is True
|
|
assert promoted.metadata["vllm_observed_workflow_count"] == 2
|
|
assert promoted.metadata["cache_hit"] is False
|
|
assert warm_throughput.metadata["cache_hit"] is True
|
|
assert [backend.mode for backend in loaded_backends] == ["eager", "throughput"]
|
|
assert loaded_backends[0].unloaded is True
|
|
assert len(comfy_memory_releases) == 2
|
|
assert any(
|
|
event.stage == "engine"
|
|
and event.message == "Optimizing vLLM for repeat workflows"
|
|
for event in progress
|
|
)
|
|
|
|
|
|
def test_resident_manager_waits_for_post_eviction_memory_recovery(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Admission should poll until an asynchronous vLLM release becomes visible."""
|
|
gib = 1024**3
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="vllm",
|
|
estimated_vram_gb=67.9,
|
|
)
|
|
controller = modal_llm_runtime_module.VLLMExecutionModeController("auto")
|
|
controller.force_throughput_after_memory_recovery("workflow-2")
|
|
runtime_profile = modal_llm_runtime_module._profile_for_vllm_execution(
|
|
profile,
|
|
controller,
|
|
)
|
|
memory_samples = iter(
|
|
[
|
|
(68 * gib, 95 * gib),
|
|
(75 * gib, 95 * gib),
|
|
(85 * gib, 95 * gib),
|
|
]
|
|
)
|
|
clock = [0.0]
|
|
sleeps: list[float] = []
|
|
releases: list[int] = []
|
|
|
|
def sleep(seconds: float) -> None:
|
|
"""Advance the deterministic recovery clock."""
|
|
sleeps.append(seconds)
|
|
clock[0] += seconds
|
|
|
|
manager = modal_llm_runtime_module.ResidentLLMManager(
|
|
storage_root=tmp_path,
|
|
memory_info=lambda: next(memory_samples),
|
|
empty_cache=lambda: None,
|
|
snapshot_ready=lambda storage_root, selected_profile: True,
|
|
comfy_memory_release=releases.append,
|
|
vllm_mode_controller=controller,
|
|
memory_recovery_timeout_seconds=1.0,
|
|
memory_recovery_poll_interval_seconds=0.25,
|
|
monotonic=lambda: clock[0],
|
|
sleep=sleep,
|
|
)
|
|
|
|
manager._make_room(
|
|
runtime_profile,
|
|
16.0,
|
|
evicted_before_load=True,
|
|
)
|
|
|
|
assert len(releases) == 1
|
|
assert sleeps == [0.25, 0.25]
|
|
|
|
|
|
def test_resident_manager_marks_exhausted_post_eviction_memory_for_retry(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A bounded recovery timeout should emit the cross-process retry marker."""
|
|
gib = 1024**3
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="vllm",
|
|
estimated_vram_gb=67.9,
|
|
)
|
|
controller = modal_llm_runtime_module.VLLMExecutionModeController("auto")
|
|
controller.force_throughput_after_memory_recovery("workflow-2")
|
|
runtime_profile = modal_llm_runtime_module._profile_for_vllm_execution(
|
|
profile,
|
|
controller,
|
|
)
|
|
clock = [0.0]
|
|
|
|
def sleep(seconds: float) -> None:
|
|
"""Advance the deterministic recovery clock."""
|
|
clock[0] += seconds
|
|
|
|
manager = modal_llm_runtime_module.ResidentLLMManager(
|
|
storage_root=tmp_path,
|
|
memory_info=lambda: (68 * gib, 95 * gib),
|
|
empty_cache=lambda: None,
|
|
snapshot_ready=lambda storage_root, selected_profile: True,
|
|
comfy_memory_release=lambda required_bytes: None,
|
|
vllm_mode_controller=controller,
|
|
memory_recovery_timeout_seconds=0.5,
|
|
memory_recovery_poll_interval_seconds=0.25,
|
|
monotonic=lambda: clock[0],
|
|
sleep=sleep,
|
|
)
|
|
|
|
with pytest.raises(
|
|
RuntimeError,
|
|
match="comfy-modal-llm-memory-recovery-exhausted.*vllm_mode=throughput",
|
|
):
|
|
manager._make_room(
|
|
runtime_profile,
|
|
16.0,
|
|
evicted_before_load=True,
|
|
)
|
|
|
|
assert clock[0] == 0.5
|
|
|
|
|
|
def test_representative_llm_prewarm_inputs_cover_text_and_vision_shapes(
|
|
modal_llm_runtime_module: Any,
|
|
) -> None:
|
|
"""JIT warmup should exercise text plus two bounded image resolutions."""
|
|
profile = SimpleNamespace(modalities=frozenset({"text", "image"}))
|
|
|
|
text_inputs = modal_llm_runtime_module._representative_prewarm_inputs(profile, 0)
|
|
small_image_inputs = modal_llm_runtime_module._representative_prewarm_inputs(
|
|
profile, 1
|
|
)
|
|
large_image_inputs = modal_llm_runtime_module._representative_prewarm_inputs(
|
|
profile, 2
|
|
)
|
|
|
|
assert text_inputs.images == ()
|
|
assert small_image_inputs.images[0].size == (512, 512)
|
|
assert large_image_inputs.images[0].size == (1024, 1024)
|
|
|
|
|
|
def test_vllm_backend_translates_private_runtime_errors(
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Remote failures should not require vLLM exception classes on the client."""
|
|
|
|
class FakeProcessor:
|
|
"""Return a deterministic prompt without importing Transformers."""
|
|
|
|
def apply_chat_template(self, messages: Any, **kwargs: Any) -> str:
|
|
"""Ignore the request structure and return a prompt sentinel."""
|
|
del messages, kwargs
|
|
return "rendered prompt"
|
|
|
|
class FakeEngineDeadError(Exception):
|
|
"""Stand in for vLLM's non-RuntimeError engine exception."""
|
|
|
|
class FailingLLM:
|
|
"""Raise the RuntimeError shape used by vLLM engine failures."""
|
|
|
|
async def generate(self, prompts: Any, **kwargs: Any) -> Any:
|
|
"""Fail after accepting a well-formed generation request."""
|
|
del prompts, kwargs
|
|
raise FakeEngineDeadError("engine subprocess stopped")
|
|
yield
|
|
|
|
async def abort(self, request_id: str) -> None:
|
|
"""Accept cleanup after the failed request."""
|
|
del request_id
|
|
|
|
class FakeSamplingParams:
|
|
"""Accept backend-neutral generation options without vLLM installed."""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
"""Discard values after proving the adapter constructed them."""
|
|
del kwargs
|
|
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"vllm",
|
|
SimpleNamespace(SamplingParams=FakeSamplingParams, __path__=[]),
|
|
)
|
|
monkeypatch.setitem(sys.modules, "vllm.v1", SimpleNamespace(__path__=[]))
|
|
monkeypatch.setitem(sys.modules, "vllm.v1.engine", SimpleNamespace(__path__=[]))
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"vllm.v1.engine.exceptions",
|
|
SimpleNamespace(EngineDeadError=FakeEngineDeadError),
|
|
)
|
|
|
|
backend = object.__new__(modal_llm_runtime_module.VLLMMultimodalBackend)
|
|
backend.profile = SimpleNamespace(
|
|
profile_id="generated-profile",
|
|
architecture="",
|
|
reasoning_parser="",
|
|
backend_option=lambda name, default=None: default,
|
|
)
|
|
backend.processor = FakeProcessor()
|
|
backend.llm = FailingLLM()
|
|
backend.reasoning_parser = SimpleNamespace(requires_boundary_tokens=False)
|
|
prepared = modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
)
|
|
settings = modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=0,
|
|
)
|
|
|
|
with pytest.raises(
|
|
RuntimeError,
|
|
match="vLLM generation failed for profile 'generated-profile'",
|
|
):
|
|
asyncio.run(backend._generate_async(prepared, settings, lambda progress: None))
|
|
|
|
|
|
def test_vllm_request_disables_thinking_and_boundary_parsing(
|
|
modal_llm_runtime_module: Any,
|
|
llm_reasoning_module: Any,
|
|
) -> None:
|
|
"""The vLLM request and decoder should share the disabled reasoning state."""
|
|
observed: dict[str, Any] = {}
|
|
|
|
class FakeProcessor:
|
|
"""Capture the per-request chat-template controls."""
|
|
|
|
def apply_chat_template(self, messages: Any, **kwargs: Any) -> str:
|
|
"""Record template arguments and return a direct-response prompt."""
|
|
del messages
|
|
observed.update(kwargs)
|
|
return "rendered direct prompt"
|
|
|
|
class FakeSamplingParams:
|
|
"""Retain sampling arguments for assertions."""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
"""Store the backend adapter's keyword arguments."""
|
|
self.kwargs = kwargs
|
|
|
|
backend = object.__new__(modal_llm_runtime_module.VLLMMultimodalBackend)
|
|
backend.profile = SimpleNamespace(
|
|
architecture="Qwen3_5ForConditionalGeneration",
|
|
reasoning_parser="qwen3",
|
|
)
|
|
backend.processor = FakeProcessor()
|
|
backend.reasoning_parser = llm_reasoning_module.Qwen3ReasoningParser(
|
|
_FakeReasoningTokenizer()
|
|
)
|
|
prepared = modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
)
|
|
settings = modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=0,
|
|
enable_reasoning=False,
|
|
)
|
|
|
|
request = backend._request(prepared, settings)
|
|
sampling_params = backend._sampling_params(FakeSamplingParams, settings)
|
|
|
|
assert request == {"prompt": "rendered direct prompt"}
|
|
assert observed["enable_thinking"] is False
|
|
assert sampling_params.kwargs["skip_special_tokens"] is True
|
|
|
|
|
|
def test_vllm_backend_aborts_when_progress_callback_cancels(
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""ComfyUI cancellation during streaming should abort the vLLM request."""
|
|
aborted_requests: list[str] = []
|
|
|
|
class FakeSamplingParams:
|
|
"""Accept generation options for the cancellation path."""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
"""Discard validated generation options."""
|
|
del kwargs
|
|
|
|
class FakeEngineDeadError(Exception):
|
|
"""Stand in for vLLM's private engine exception."""
|
|
|
|
class StreamingLLM:
|
|
"""Yield one token update and record its subsequent abort."""
|
|
|
|
async def generate(self, prompt: Any, **kwargs: Any) -> Any:
|
|
"""Yield one cumulative request output."""
|
|
del prompt, kwargs
|
|
yield SimpleNamespace(
|
|
prompt_token_ids=[1],
|
|
outputs=[SimpleNamespace(text="partial", token_ids=[2])],
|
|
)
|
|
|
|
async def abort(self, request_id: str) -> None:
|
|
"""Record the interrupted request id."""
|
|
aborted_requests.append(request_id)
|
|
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"vllm",
|
|
SimpleNamespace(SamplingParams=FakeSamplingParams, __path__=[]),
|
|
)
|
|
monkeypatch.setitem(sys.modules, "vllm.v1", SimpleNamespace(__path__=[]))
|
|
monkeypatch.setitem(sys.modules, "vllm.v1.engine", SimpleNamespace(__path__=[]))
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"vllm.v1.engine.exceptions",
|
|
SimpleNamespace(EngineDeadError=FakeEngineDeadError),
|
|
)
|
|
backend = object.__new__(modal_llm_runtime_module.VLLMMultimodalBackend)
|
|
backend.profile = SimpleNamespace(
|
|
profile_id="generated-profile",
|
|
reasoning_parser="",
|
|
architecture="",
|
|
)
|
|
backend.processor = SimpleNamespace(
|
|
apply_chat_template=lambda *args, **kwargs: "rendered prompt"
|
|
)
|
|
backend.llm = StreamingLLM()
|
|
backend.reasoning_parser = SimpleNamespace(requires_boundary_tokens=False)
|
|
prepared = modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
)
|
|
settings = modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=0,
|
|
)
|
|
|
|
def cancel_on_token(progress: Any) -> None:
|
|
"""Raise the same interruption shape used by the node callback."""
|
|
if progress.stage == "generating":
|
|
raise InterruptedError("cancelled")
|
|
|
|
with pytest.raises(InterruptedError, match="cancelled"):
|
|
asyncio.run(backend._generate_async(prepared, settings, cancel_on_token))
|
|
|
|
assert len(aborted_requests) == 1
|
|
assert aborted_requests[0].startswith("modal-llm-")
|
|
|
|
|
|
def test_local_storage_uses_comfyui_model_directory(
|
|
local_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Local snapshots should follow ComfyUI's configured model root."""
|
|
paths: list[str] = []
|
|
fake_folder_paths = SimpleNamespace(
|
|
models_dir=str(tmp_path / "models"),
|
|
add_model_folder_path=lambda name, path, is_default=False: paths.append(
|
|
f"{name}:{path}:{is_default}"
|
|
),
|
|
get_folder_paths=lambda name: [str(tmp_path / "models" / name)],
|
|
)
|
|
monkeypatch.delenv("COMFY_MODAL_LOCAL_LLM_STORAGE_ROOT", raising=False)
|
|
monkeypatch.setitem(sys.modules, "folder_paths", fake_folder_paths)
|
|
|
|
storage_root = local_llm_runtime_module.local_llm_storage_root()
|
|
|
|
assert storage_root == (tmp_path / "models" / "modal_llm").resolve()
|
|
assert paths == [f"modal_llm:{(tmp_path / 'models' / 'modal_llm').resolve()}:True"]
|
|
|
|
|
|
def test_local_runtime_rejects_non_apple_hardware_actionably(
|
|
local_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Unsupported local hardware should direct the workflow to Modal."""
|
|
monkeypatch.setattr(local_llm_runtime_module.platform, "system", lambda: "Linux")
|
|
monkeypatch.setattr(local_llm_runtime_module.platform, "machine", lambda: "x86_64")
|
|
|
|
with pytest.raises(RuntimeError, match="Enable 'Run on Modal'"):
|
|
local_llm_runtime_module.ensure_local_apple_runtime_available()
|
|
|
|
|
|
def test_local_runtime_requires_the_pinned_mlx_version(
|
|
local_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A drifted MLX adapter should fail with an exact repair command."""
|
|
monkeypatch.setattr(local_llm_runtime_module.platform, "system", lambda: "Darwin")
|
|
monkeypatch.setattr(local_llm_runtime_module.platform, "machine", lambda: "arm64")
|
|
monkeypatch.setattr(
|
|
local_llm_runtime_module.metadata,
|
|
"version",
|
|
lambda distribution: "0.1.0",
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="mlx-vlm==0.6.15"):
|
|
local_llm_runtime_module.ensure_local_apple_runtime_available()
|
|
|
|
|
|
def test_local_curated_profile_is_staged_with_mlx_backend(
|
|
local_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A saved curated id should stage locally without changing the workflow."""
|
|
staged: list[tuple[str, Any, Any]] = []
|
|
monkeypatch.setattr(
|
|
local_llm_runtime_module,
|
|
"stage_model_profile",
|
|
lambda profile_id, storage_root, **kwargs: staged.append(
|
|
(profile_id, Path(storage_root), kwargs["profile"])
|
|
),
|
|
)
|
|
|
|
profile = local_llm_runtime_module.resolve_and_stage_local_profile(
|
|
"smolvlm2-2.2b-instruct",
|
|
tmp_path,
|
|
progress_callback=lambda progress: None,
|
|
)
|
|
|
|
assert profile.backend == "mlx_vlm"
|
|
assert profile.execution_target == "local_apple"
|
|
assert staged == [(profile.profile_id, tmp_path, profile)]
|
|
|
|
|
|
def test_mlx_backend_streams_multimodal_reasoning_and_progress(
|
|
local_llm_runtime_module: Any,
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""MLX streaming should retain the shared outputs and reasoning contract."""
|
|
import types
|
|
|
|
observed: dict[str, Any] = {}
|
|
|
|
class FakeMX:
|
|
"""Expose only the MLX lifecycle methods used by the adapter."""
|
|
|
|
@staticmethod
|
|
def reset_peak_memory() -> None:
|
|
"""Record peak-memory reset."""
|
|
observed["reset_peak_memory"] = True
|
|
|
|
@staticmethod
|
|
def clear_cache() -> None:
|
|
"""Record cache release."""
|
|
observed["clear_cache"] = True
|
|
|
|
processor = SimpleNamespace(tokenizer=_FakeReasoningTokenizer())
|
|
model = SimpleNamespace(config=SimpleNamespace(model_type="qwen3_5"))
|
|
|
|
def fake_load(path: str, **kwargs: Any) -> tuple[Any, Any]:
|
|
"""Return a deterministic resident MLX model and processor."""
|
|
observed["load"] = (path, kwargs)
|
|
return model, processor
|
|
|
|
def fake_apply_chat_template(
|
|
selected_processor: Any,
|
|
config: Any,
|
|
messages: Any,
|
|
**kwargs: Any,
|
|
) -> str:
|
|
"""Capture the structured prompt passed to MLX-VLM."""
|
|
observed["chat"] = (selected_processor, config, messages, kwargs)
|
|
return "rendered prompt"
|
|
|
|
def fake_stream_generate(*args: Any, **kwargs: Any) -> Any:
|
|
"""Yield exact reasoning boundary and response tokens."""
|
|
observed["generate"] = (args, kwargs)
|
|
segments = (
|
|
[
|
|
("<think>", 10),
|
|
("consider", 1),
|
|
("</think>", 11),
|
|
("final answer", 3),
|
|
]
|
|
if kwargs["enable_thinking"]
|
|
else [("direct answer", 3)]
|
|
)
|
|
for index, (text, token) in enumerate(segments, start=1):
|
|
yield SimpleNamespace(
|
|
text=text,
|
|
token=token,
|
|
prompt_tokens=5,
|
|
generation_tokens=index,
|
|
generation_tps=10.0 + index,
|
|
)
|
|
|
|
mlx_module = types.ModuleType("mlx")
|
|
mlx_module.__path__ = []
|
|
mlx_core_module = types.ModuleType("mlx.core")
|
|
mlx_core_module.reset_peak_memory = FakeMX.reset_peak_memory
|
|
mlx_core_module.clear_cache = FakeMX.clear_cache
|
|
mlx_vlm_module = types.ModuleType("mlx_vlm")
|
|
mlx_vlm_module.__path__ = []
|
|
mlx_vlm_module.load = fake_load
|
|
mlx_generate_module = types.ModuleType("mlx_vlm.generate")
|
|
mlx_generate_module.stream_generate = fake_stream_generate
|
|
mlx_prompt_module = types.ModuleType("mlx_vlm.prompt_utils")
|
|
mlx_prompt_module.apply_chat_template = fake_apply_chat_template
|
|
monkeypatch.setitem(sys.modules, "mlx", mlx_module)
|
|
monkeypatch.setitem(sys.modules, "mlx.core", mlx_core_module)
|
|
monkeypatch.setitem(sys.modules, "mlx_vlm", mlx_vlm_module)
|
|
monkeypatch.setitem(sys.modules, "mlx_vlm.generate", mlx_generate_module)
|
|
monkeypatch.setitem(sys.modules, "mlx_vlm.prompt_utils", mlx_prompt_module)
|
|
monkeypatch.setattr(
|
|
local_llm_runtime_module,
|
|
"ensure_local_apple_runtime_available",
|
|
lambda: None,
|
|
)
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="mlx_vlm",
|
|
architecture="Qwen3_5ForConditionalGeneration",
|
|
reasoning_parser="qwen3",
|
|
execution_target="local_apple",
|
|
max_context_tokens=128,
|
|
)
|
|
progress: list[Any] = []
|
|
backend = local_llm_runtime_module.MLXVLMBackend(
|
|
profile,
|
|
tmp_path,
|
|
progress.append,
|
|
)
|
|
prepared = modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="describe",
|
|
system_prompt="be concise",
|
|
images=("image-a",),
|
|
video=modal_llm_runtime_module.PreparedVideo(
|
|
frames=("frame-a", "frame-b"),
|
|
timestamps_seconds=(0.0, 1.0),
|
|
),
|
|
file_characters=0,
|
|
file_count=0,
|
|
)
|
|
|
|
result = backend.generate(
|
|
prepared,
|
|
modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=0.95,
|
|
seed=7,
|
|
),
|
|
progress.append,
|
|
)
|
|
reasoning_chat = observed["chat"]
|
|
reasoning_generate = observed["generate"]
|
|
direct_result = backend.generate(
|
|
prepared,
|
|
modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=0.95,
|
|
seed=7,
|
|
enable_reasoning=False,
|
|
),
|
|
progress.append,
|
|
)
|
|
direct_chat = observed["chat"]
|
|
direct_generate = observed["generate"]
|
|
backend.unload()
|
|
|
|
assert result.text == "final answer"
|
|
assert result.reasoning == "consider"
|
|
assert result.reasoning_tokens == 1
|
|
assert result.input_tokens == 5
|
|
assert result.output_tokens == 4
|
|
assert reasoning_chat[2] == [
|
|
{"role": "system", "content": "be concise"},
|
|
{"role": "user", "content": "describe"},
|
|
]
|
|
assert reasoning_chat[3]["num_images"] == 3
|
|
assert reasoning_chat[3]["enable_thinking"] is True
|
|
assert reasoning_generate[1]["image"] == [
|
|
"image-a",
|
|
"frame-a",
|
|
"frame-b",
|
|
]
|
|
assert reasoning_generate[1]["seed"] == 7
|
|
assert reasoning_generate[1]["enable_thinking"] is True
|
|
assert reasoning_generate[1]["thinking_budget"] == 4
|
|
assert direct_result.text == "direct answer"
|
|
assert direct_result.reasoning == ""
|
|
assert direct_result.reasoning_parser == "disabled"
|
|
assert direct_chat[3]["enable_thinking"] is False
|
|
assert direct_generate[1]["enable_thinking"] is False
|
|
assert "thinking_budget" not in direct_generate[1]
|
|
assert direct_generate[1]["skip_special_tokens"] is True
|
|
assert [event.value for event in progress if event.stage == "generating"] == [
|
|
1,
|
|
2,
|
|
3,
|
|
4,
|
|
1,
|
|
]
|
|
assert observed["clear_cache"] is True
|
|
|
|
|
|
@dataclass
|
|
class _FakeBackend:
|
|
"""Record resident backend inference and unload behavior."""
|
|
|
|
profile_id: str
|
|
unloaded: bool = False
|
|
generate_calls: int = 0
|
|
|
|
def generate(
|
|
self,
|
|
prepared_inputs: Any,
|
|
settings: Any,
|
|
progress_callback: Callable[[Any], None],
|
|
) -> Any:
|
|
"""Return deterministic token counts while exercising progress."""
|
|
del prepared_inputs
|
|
self.generate_calls += 1
|
|
progress_callback(SimpleNamespace(stage="generating", value=1))
|
|
return SimpleNamespace(
|
|
text=f"response:{self.profile_id}:{settings.seed}",
|
|
input_tokens=7,
|
|
output_tokens=1,
|
|
reasoning="",
|
|
reasoning_tokens=0,
|
|
reasoning_parser="none",
|
|
time_to_first_token_seconds=0.25,
|
|
tokens_per_second=4.0,
|
|
)
|
|
|
|
def unload(self) -> None:
|
|
"""Record that the cache released this backend."""
|
|
self.unloaded = True
|
|
|
|
|
|
def test_resident_manager_reuses_and_lru_evicts_models(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Warm requests should hit cache and evict the least-recently used model."""
|
|
base_profile = llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct")
|
|
second_profile = replace(
|
|
base_profile,
|
|
profile_id="second-profile",
|
|
revision="1" * 40,
|
|
)
|
|
backends: dict[str, _FakeBackend] = {}
|
|
|
|
def backend_factory(
|
|
profile: Any,
|
|
snapshot_path: Path,
|
|
progress_callback: Callable[[Any], None],
|
|
) -> _FakeBackend:
|
|
"""Create one fake backend per load."""
|
|
del snapshot_path, progress_callback
|
|
backend = _FakeBackend(profile.profile_id)
|
|
backends[profile.profile_id] = backend
|
|
return backend
|
|
|
|
manager = modal_llm_runtime_module.ResidentLLMManager(
|
|
storage_root=tmp_path,
|
|
backend_factory=backend_factory,
|
|
max_resident_models=1,
|
|
memory_info=lambda: (200 * 1024**3, 256 * 1024**3),
|
|
empty_cache=lambda: None,
|
|
snapshot_ready=lambda storage_root, profile: True,
|
|
comfy_memory_release=lambda required_bytes: None,
|
|
)
|
|
prepared = modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
)
|
|
settings = modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=3,
|
|
)
|
|
|
|
first = manager.infer(
|
|
profile=base_profile,
|
|
prepared_inputs=prepared,
|
|
generation_settings=settings,
|
|
reserve_free_vram_gb=0,
|
|
keep_model_loaded=True,
|
|
progress_callback=lambda value: None,
|
|
)
|
|
second = manager.infer(
|
|
profile=base_profile,
|
|
prepared_inputs=prepared,
|
|
generation_settings=settings,
|
|
reserve_free_vram_gb=0,
|
|
keep_model_loaded=True,
|
|
progress_callback=lambda value: None,
|
|
)
|
|
manager.infer(
|
|
profile=second_profile,
|
|
prepared_inputs=prepared,
|
|
generation_settings=settings,
|
|
reserve_free_vram_gb=0,
|
|
keep_model_loaded=True,
|
|
progress_callback=lambda value: None,
|
|
)
|
|
|
|
assert first.metadata["cache_hit"] is False
|
|
assert first.metadata["reasoning_enabled"] is True
|
|
assert first.metadata["execution_target"] == "modal"
|
|
assert first.metadata["device"] == "cuda"
|
|
assert first.metadata["memory_total_gib"] == 256
|
|
assert first.metadata["gpu_total_gib"] == 256
|
|
assert second.metadata["cache_hit"] is True
|
|
assert backends[base_profile.profile_id].generate_calls == 2
|
|
assert backends[base_profile.profile_id].unloaded is True
|
|
assert manager.resident_profiles() == (second_profile.profile_id,)
|
|
|
|
|
|
def test_resident_manager_reports_apple_unified_memory_metadata(
|
|
modal_llm_runtime_module: Any,
|
|
llm_profiles_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Local inference telemetry should not mislabel unified memory as VRAM."""
|
|
profile = replace(
|
|
llm_profiles_module.get_llm_profile("smolvlm2-2.2b-instruct"),
|
|
backend="mlx_vlm",
|
|
execution_target="local_apple",
|
|
)
|
|
manager = modal_llm_runtime_module.ResidentLLMManager(
|
|
storage_root=tmp_path,
|
|
backend_factory=lambda *args: _FakeBackend(profile.profile_id),
|
|
memory_info=lambda: (48 * 1024**3, 64 * 1024**3),
|
|
empty_cache=lambda: None,
|
|
snapshot_ready=lambda storage_root, selected_profile: True,
|
|
comfy_memory_release=lambda required_bytes: None,
|
|
execution_target="local_apple",
|
|
device_name="metal",
|
|
memory_label="unified memory",
|
|
)
|
|
|
|
result = manager.infer(
|
|
profile=profile,
|
|
prepared_inputs=modal_llm_runtime_module.PreparedLLMInputs(
|
|
prompt="hello",
|
|
system_prompt="",
|
|
images=(),
|
|
video=None,
|
|
file_characters=0,
|
|
file_count=0,
|
|
),
|
|
generation_settings=modal_llm_runtime_module.LLMGenerationSettings(
|
|
max_new_tokens=8,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
seed=0,
|
|
),
|
|
reserve_free_vram_gb=1,
|
|
keep_model_loaded=False,
|
|
progress_callback=lambda progress: None,
|
|
)
|
|
|
|
assert result.metadata["execution_target"] == "local_apple"
|
|
assert result.metadata["device"] == "metal"
|
|
assert result.metadata["memory_total_gib"] == 64
|
|
assert result.metadata["memory_available_before_gib"] == 48
|
|
assert "gpu_total_gib" not in result.metadata
|
|
|
|
|
|
def test_modal_llm_node_routes_between_local_and_remote_runtimes(
|
|
modal_llm_node_module: Any,
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The V3 node toggle state should select local or remote execution."""
|
|
schema = modal_llm_node_module.ModalLLM.define_schema()
|
|
assert schema.node_id == "ModalLLM"
|
|
assert [output.display_name for output in schema.outputs] == [
|
|
"response",
|
|
"metadata_json",
|
|
"reasoning",
|
|
]
|
|
assert "enable_reasoning" in [input_value.id for input_value in schema.inputs]
|
|
|
|
calls: list[tuple[str, float, bool]] = []
|
|
|
|
def fake_result(target: str, **kwargs: Any) -> Any:
|
|
"""Return deterministic output while recording the selected runtime."""
|
|
calls.append(
|
|
(
|
|
target,
|
|
kwargs["reserve_free_vram_gb"],
|
|
kwargs["enable_reasoning"],
|
|
)
|
|
)
|
|
return modal_llm_runtime_module.LLMInferenceResult(
|
|
text=f"{target}-done",
|
|
metadata={
|
|
"output_tokens": 1,
|
|
"cache_hit": True,
|
|
"execution_target": target,
|
|
},
|
|
reasoning="working",
|
|
)
|
|
|
|
monkeypatch.delenv("COMFY_MODAL_REMOTE_WORKER", raising=False)
|
|
monkeypatch.setattr(
|
|
modal_llm_node_module,
|
|
"run_local_llm_inference",
|
|
lambda **kwargs: fake_result("local_apple", **kwargs),
|
|
)
|
|
local_output = modal_llm_node_module.ModalLLM.execute(
|
|
prompt="hello",
|
|
model_profile="smolvlm2-2.2b-instruct",
|
|
max_new_tokens=4,
|
|
local_reserve_free_memory_gb=3.0,
|
|
)
|
|
monkeypatch.setenv("COMFY_MODAL_REMOTE_WORKER", "1")
|
|
monkeypatch.setattr(
|
|
modal_llm_node_module,
|
|
"run_modal_llm_inference",
|
|
lambda **kwargs: fake_result("modal", **kwargs),
|
|
)
|
|
remote_output = modal_llm_node_module.ModalLLM.execute(
|
|
prompt="hello",
|
|
model_profile="smolvlm2-2.2b-instruct",
|
|
enable_reasoning=False,
|
|
max_new_tokens=4,
|
|
reserve_free_vram_gb=17.0,
|
|
)
|
|
|
|
assert local_output.result[0] == "local_apple-done"
|
|
assert json.loads(local_output.result[1])["execution_target"] == "local_apple"
|
|
assert remote_output.result[0] == "modal-done"
|
|
assert json.loads(remote_output.result[1])["execution_target"] == "modal"
|
|
assert remote_output.result[2] == "working"
|
|
assert calls == [
|
|
("local_apple", 3.0, True),
|
|
("modal", 17.0, False),
|
|
]
|
|
|
|
|
|
def test_resident_manager_reports_ssh_docker_execution_target(
|
|
modal_llm_runtime_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A self-hosted CUDA worker must not identify its inference as Modal."""
|
|
monkeypatch.setenv("COMFY_MODAL_LLM_EXECUTION_TARGET", "ssh_docker")
|
|
monkeypatch.setenv("COMFY_MODAL_REMOTE_STORAGE_ROOT", str(tmp_path))
|
|
monkeypatch.setattr(modal_llm_runtime_module, "_RESIDENT_MANAGER", None)
|
|
|
|
manager = modal_llm_runtime_module.get_resident_llm_manager()
|
|
|
|
assert manager.execution_target == "ssh_docker"
|
|
assert manager.device_name == "cuda (SSH Docker)"
|
|
|
|
|
|
def test_remote_dispatch_stages_llm_once_and_forces_volume_reload(
|
|
remote_modal_app_module: Any,
|
|
modal_llm_profile_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The local dispatcher should finish CPU staging before sending work to a GPU."""
|
|
stage_calls: list[list[str]] = []
|
|
|
|
class FakeStageMethod:
|
|
"""Expose the Modal method remote-call surface."""
|
|
|
|
def remote(self, profile_ids: list[str]) -> list[dict[str, str]]:
|
|
"""Record and confirm every requested profile."""
|
|
stage_calls.append(profile_ids)
|
|
return [
|
|
{
|
|
"requested_reference": profile_id,
|
|
"profile_id": profile_id,
|
|
"revision": "482adb537c021c86670beed01cd58990d01e72e4",
|
|
}
|
|
for profile_id in profile_ids
|
|
]
|
|
|
|
class FakeStager:
|
|
"""Expose the deployed CPU staging method."""
|
|
|
|
stage_profiles = FakeStageMethod()
|
|
|
|
class FakeRemoteClass:
|
|
"""Construct one fake deployed stager instance."""
|
|
|
|
def __call__(self) -> FakeStager:
|
|
"""Return a fake stager."""
|
|
return FakeStager()
|
|
|
|
class FakeCls:
|
|
"""Resolve only the expected deployed stager class."""
|
|
|
|
@staticmethod
|
|
def from_name(app_name: str, class_name: str) -> FakeRemoteClass:
|
|
"""Return a fake class handle after validating lookup identity."""
|
|
assert app_name == "test-b300-app"
|
|
assert class_name == "ModelStager"
|
|
return FakeRemoteClass()
|
|
|
|
monkeypatch.setattr(
|
|
modal_llm_profile_staging_module,
|
|
"modal",
|
|
SimpleNamespace(Cls=FakeCls),
|
|
)
|
|
with modal_llm_profile_staging_module._STAGED_LLM_PROFILES_LOCK:
|
|
modal_llm_profile_staging_module._STAGED_LLM_PROFILES.clear()
|
|
modal_llm_profile_staging_module._STAGED_LLM_PROFILE_RESULTS.clear()
|
|
payload = {
|
|
"component_id": "llm-component",
|
|
"subgraph_prompt": {
|
|
"1": {
|
|
"class_type": "ModalLLM",
|
|
"inputs": {"model_profile": "smolvlm2-2.2b-instruct"},
|
|
}
|
|
},
|
|
}
|
|
|
|
modal_llm_profile_staging_module._ensure_llm_profiles_staged(payload, "test-b300-app")
|
|
first_marker = payload["volume_reload_marker"]
|
|
modal_llm_profile_staging_module._ensure_llm_profiles_staged(payload, "test-b300-app")
|
|
|
|
assert stage_calls == [["smolvlm2-2.2b-instruct"]]
|
|
assert payload["requires_volume_reload"] is True
|
|
assert payload["volume_reload_marker"] == first_marker
|
|
|
|
|
|
def test_remote_dispatch_rewrites_hugging_face_id_to_generated_profile(
|
|
remote_modal_app_module: Any,
|
|
modal_llm_profile_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""GPU payloads should contain only immutable generated profile IDs."""
|
|
requested_model = "owner/model"
|
|
generated_profile_id = "hf-" + "a" * 64
|
|
|
|
class FakeStageMethod:
|
|
"""Resolve one user model ID into an immutable profile."""
|
|
|
|
def remote(self, model_references: list[str]) -> list[dict[str, Any]]:
|
|
"""Return one generated profile result."""
|
|
assert model_references == [requested_model]
|
|
return [
|
|
{
|
|
"requested_reference": requested_model,
|
|
"profile_id": generated_profile_id,
|
|
"revision": "7" * 40,
|
|
"downloaded": True,
|
|
}
|
|
]
|
|
|
|
class FakeCls:
|
|
"""Resolve the CPU ModelStager class."""
|
|
|
|
@staticmethod
|
|
def from_name(app_name: str, class_name: str) -> Callable[[], Any]:
|
|
"""Return a staging class constructor."""
|
|
assert app_name == "test-b300-app"
|
|
assert class_name == "ModelStager"
|
|
return lambda: SimpleNamespace(stage_profiles=FakeStageMethod())
|
|
|
|
monkeypatch.setattr(
|
|
modal_llm_profile_staging_module,
|
|
"modal",
|
|
SimpleNamespace(Cls=FakeCls),
|
|
)
|
|
with modal_llm_profile_staging_module._STAGED_LLM_PROFILES_LOCK:
|
|
modal_llm_profile_staging_module._STAGED_LLM_PROFILES.clear()
|
|
modal_llm_profile_staging_module._STAGED_LLM_PROFILE_RESULTS.clear()
|
|
payload = {
|
|
"component_id": "dynamic-llm",
|
|
"subgraph_prompt": {
|
|
"1": {
|
|
"class_type": "ModalLLM",
|
|
"inputs": {"model_profile": requested_model},
|
|
}
|
|
},
|
|
}
|
|
|
|
modal_llm_profile_staging_module._ensure_llm_profiles_staged(payload, "test-b300-app")
|
|
direct_inputs = remote_modal_app_module.serialize_node_inputs(
|
|
{"model_profile": requested_model, "prompt": "hello"}
|
|
)
|
|
rewritten_inputs = remote_modal_app_module.deserialize_node_inputs(
|
|
modal_llm_profile_staging_module._rewrite_staged_llm_kwargs_payload(
|
|
direct_inputs,
|
|
"test-b300-app",
|
|
)
|
|
)
|
|
|
|
assert (
|
|
payload["subgraph_prompt"]["1"]["inputs"]["model_profile"]
|
|
== generated_profile_id
|
|
)
|
|
assert rewritten_inputs == {
|
|
"model_profile": generated_profile_id,
|
|
"prompt": "hello",
|
|
}
|
|
assert payload["requires_volume_reload"] is True
|
|
|
|
|
|
def test_remote_dispatch_streams_cpu_llm_staging_progress(
|
|
remote_modal_app_module: Any,
|
|
modal_llm_profile_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""First-run Hugging Face progress should render before GPU allocation."""
|
|
observed_progress: list[dict[str, Any]] = []
|
|
|
|
class FakeStageStream:
|
|
"""Yield one download update followed by the immutable profile result."""
|
|
|
|
def remote_gen(self, model_references: list[str]) -> Any:
|
|
"""Stream deterministic CPU staging envelopes."""
|
|
assert model_references == ["owner/model"]
|
|
yield {
|
|
"kind": "progress",
|
|
"stage": "download",
|
|
"message": "Fetching 8 files",
|
|
"value": 3,
|
|
"max": 8,
|
|
"unit": "files",
|
|
"model_reference": "owner/model",
|
|
}
|
|
yield {
|
|
"kind": "result",
|
|
"results": [
|
|
{
|
|
"requested_reference": "owner/model",
|
|
"profile_id": "hf-" + "b" * 64,
|
|
"revision": "8" * 40,
|
|
}
|
|
],
|
|
}
|
|
|
|
class FakeCls:
|
|
"""Resolve the streaming CPU ModelStager."""
|
|
|
|
@staticmethod
|
|
def from_name(app_name: str, class_name: str) -> Callable[[], Any]:
|
|
"""Return a deterministic streaming stager instance."""
|
|
assert app_name == "test-app"
|
|
assert class_name == "ModelStager"
|
|
return lambda: SimpleNamespace(stage_profiles_stream=FakeStageStream())
|
|
|
|
monkeypatch.setattr(
|
|
modal_llm_profile_staging_module,
|
|
"modal",
|
|
SimpleNamespace(Cls=FakeCls),
|
|
)
|
|
monkeypatch.setattr(
|
|
modal_llm_profile_staging_module,
|
|
"_emit_local_llm_staging_progress",
|
|
lambda payload, event: observed_progress.append(dict(event)),
|
|
)
|
|
with modal_llm_profile_staging_module._STAGED_LLM_PROFILES_LOCK:
|
|
modal_llm_profile_staging_module._STAGED_LLM_PROFILES.clear()
|
|
modal_llm_profile_staging_module._STAGED_LLM_PROFILE_RESULTS.clear()
|
|
payload = {
|
|
"prompt_id": "prompt-1",
|
|
"component_id": "llm-node",
|
|
"extra_data": {"client_id": "client-1"},
|
|
"subgraph_prompt": {
|
|
"llm-node": {
|
|
"class_type": "ModalLLM",
|
|
"inputs": {"model_profile": "owner/model"},
|
|
}
|
|
},
|
|
}
|
|
|
|
modal_llm_profile_staging_module._ensure_llm_profiles_staged(payload, "test-app")
|
|
|
|
assert observed_progress == [
|
|
{
|
|
"kind": "progress",
|
|
"stage": "download",
|
|
"message": "Fetching 8 files",
|
|
"value": 3,
|
|
"max": 8,
|
|
"unit": "files",
|
|
"model_reference": "owner/model",
|
|
}
|
|
]
|
|
assert (
|
|
payload["subgraph_prompt"]["llm-node"]["inputs"]["model_profile"]
|
|
== "hf-" + "b" * 64
|
|
)
|
|
|
|
|
|
def test_modal_staging_stream_has_bounded_no_progress_wait(
|
|
remote_modal_app_module: Any,
|
|
modal_llm_profile_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A silent Modal generator should be closed instead of hanging forever."""
|
|
|
|
class SilentStream:
|
|
"""Block until the controller asks this fake remote stream to close."""
|
|
|
|
def __init__(self) -> None:
|
|
self.closed = threading.Event()
|
|
|
|
def __iter__(self) -> Any:
|
|
"""Wait without producing a staging event."""
|
|
self.closed.wait(timeout=5)
|
|
return
|
|
yield # pragma: no cover - makes this method a generator.
|
|
|
|
def close(self) -> None:
|
|
"""Model successful cancellation of the remote generator."""
|
|
self.closed.set()
|
|
|
|
monkeypatch.setenv(
|
|
"COMFY_MODAL_LLM_STAGE_NO_PROGRESS_TIMEOUT_SECONDS",
|
|
"0.05",
|
|
)
|
|
stream = SilentStream()
|
|
|
|
with pytest.raises(
|
|
remote_modal_app_module.ModalRemoteInvocationError,
|
|
match="produced no progress",
|
|
):
|
|
list(modal_llm_profile_staging_module._bounded_modal_stage_events(stream))
|
|
|
|
assert stream.closed.is_set()
|
|
|
|
|
|
def test_llm_staging_progress_targets_the_matching_llm_node(
|
|
remote_modal_app_module: Any,
|
|
modal_llm_profile_staging_module: Any,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A component representative must not receive an inner LLM staging bar."""
|
|
observed_progress: list[dict[str, Any]] = []
|
|
monkeypatch.setattr(
|
|
modal_llm_profile_staging_module,
|
|
"_emit_local_modal_progress",
|
|
lambda **kwargs: observed_progress.append(kwargs),
|
|
)
|
|
payload = {
|
|
"prompt_id": "prompt-1",
|
|
"component_id": "11",
|
|
"component_node_ids": ["11", "249:263", "289:288"],
|
|
"extra_data": {"client_id": "client-1"},
|
|
"subgraph_prompt": {
|
|
"11": {"class_type": "VAELoader", "inputs": {}},
|
|
"249:263": {
|
|
"class_type": "ModalLLM",
|
|
"inputs": {"model_profile": "owner/model"},
|
|
},
|
|
"289:288": {
|
|
"class_type": "ModalLLM",
|
|
"inputs": {"model_profile": "owner/other-model"},
|
|
},
|
|
},
|
|
}
|
|
|
|
modal_llm_profile_staging_module._emit_local_llm_staging_progress(
|
|
payload,
|
|
{
|
|
"stage": "metadata",
|
|
"message": "Inspecting Hugging Face metadata for owner/model",
|
|
"indeterminate": True,
|
|
"model_reference": "owner/model",
|
|
},
|
|
)
|
|
|
|
assert [event["node_id"] for event in observed_progress] == ["249:263"]
|
|
assert observed_progress[0]["stage"] == "metadata"
|
|
assert observed_progress[0]["pre_gpu"] is True
|
|
|
|
|
|
def test_remote_runtime_registers_deployment_owned_llm_node(
|
|
modal_cloud_module: Any,
|
|
) -> None:
|
|
"""The remote image should expose ModalLLM even when custom-node sync is absent."""
|
|
nodes_module = SimpleNamespace(
|
|
NODE_CLASS_MAPPINGS={},
|
|
NODE_DISPLAY_NAME_MAPPINGS={},
|
|
)
|
|
|
|
modal_cloud_module._register_modal_sync_runtime_nodes(nodes_module)
|
|
|
|
assert nodes_module.NODE_CLASS_MAPPINGS["ModalLLM"].__name__ == "ModalLLM"
|
|
assert nodes_module.NODE_DISPLAY_NAME_MAPPINGS["ModalLLM"] == "Modal LLM"
|