292 lines
9.6 KiB
Python
292 lines
9.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
MUSA support unit tests that do not require MUSA hardware.
|
|
|
|
These tests cover the design contract documented in
|
|
``docs/source/developer_guide/musa_support_design.rst``:
|
|
|
|
- Device detection via the registry-driven
|
|
:func:`lmcache.v1.platform._detect_device`.
|
|
- Factory dispatch in :func:`lmcache.v1.gpu_connector.CreateGPUConnector`,
|
|
including fail-fast validation when device-scoped features are requested on
|
|
accelerators without connector support.
|
|
|
|
The tests stub ``torch_device_type`` / ``torch_dev`` (rather than mutating
|
|
the global PyTorch namespace) so they run on any platform.
|
|
"""
|
|
|
|
# Standard
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
import importlib
|
|
|
|
# Third Party
|
|
import pytest
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.utils import EngineType
|
|
from lmcache.v1.config import LMCacheEngineConfig
|
|
from lmcache.v1.gpu_connector import CreateGPUConnector
|
|
from lmcache.v1.metadata import LMCacheMetadata
|
|
import lmcache as lmc
|
|
import lmcache.v1.gpu_connector as gpu_connector_module
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_metadata() -> LMCacheMetadata:
|
|
"""Minimal metadata accepted by ``CreateGPUConnector``."""
|
|
return LMCacheMetadata(
|
|
model_name="musa_support_test",
|
|
world_size=1,
|
|
local_world_size=1,
|
|
worker_id=0,
|
|
local_worker_id=0,
|
|
kv_dtype=torch.bfloat16,
|
|
kv_shape=(2, 2, 16, 8, 64),
|
|
)
|
|
|
|
|
|
def _make_config(**overrides: Any) -> LMCacheEngineConfig:
|
|
"""Default config plus the requested overrides."""
|
|
config = LMCacheEngineConfig.from_defaults(chunk_size=16)
|
|
for key, value in overrides.items():
|
|
setattr(config, key, value)
|
|
return config
|
|
|
|
|
|
class _FakeTorchDev:
|
|
"""Stand-in for ``torch.musa`` / ``torch.xpu`` / ``torch.cuda``."""
|
|
|
|
def __init__(self, device_count: int = 1) -> None:
|
|
self._device_count = device_count
|
|
|
|
def is_available(self) -> bool:
|
|
return True
|
|
|
|
def device_count(self) -> int:
|
|
return self._device_count
|
|
|
|
def current_device(self) -> int:
|
|
return 0
|
|
|
|
def set_device(self, _idx: int) -> None:
|
|
return
|
|
|
|
|
|
def _patch_device(monkeypatch: pytest.MonkeyPatch, device_type: str) -> None:
|
|
"""Pretend the current accelerator is ``device_type`` in the factory."""
|
|
monkeypatch.setattr(gpu_connector_module, "torch_device_type", device_type)
|
|
monkeypatch.setattr(gpu_connector_module, "torch_dev", _FakeTorchDev())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _detect_device
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _StubTorch:
|
|
"""Minimal stand-in for ``torch`` exposing only what ``_detect_device`` reads."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
has_musa: bool = False,
|
|
has_xpu: bool = False,
|
|
has_hpu: bool = False,
|
|
musa_available: bool = False,
|
|
xpu_available: bool = False,
|
|
hpu_available: bool = False,
|
|
) -> None:
|
|
self.cuda = SimpleNamespace(is_available=lambda: True)
|
|
if has_musa:
|
|
self.musa = SimpleNamespace(is_available=lambda: musa_available)
|
|
if has_xpu:
|
|
self.xpu = SimpleNamespace(is_available=lambda: xpu_available)
|
|
if has_hpu:
|
|
self.hpu = SimpleNamespace(is_available=lambda: hpu_available)
|
|
|
|
|
|
def _detect_with_stub(stub: _StubTorch) -> tuple[Any, str]:
|
|
"""Run ``_detect_device`` with ``torch`` swapped for the stub."""
|
|
# First Party
|
|
from lmcache.v1.platform import _detect_device
|
|
|
|
with patch.dict("sys.modules", {"torch": stub}):
|
|
return _detect_device()
|
|
|
|
|
|
def test_detect_device_prefers_musa_when_available() -> None:
|
|
"""``_detect_device`` returns MUSA whenever ``torch.musa.is_available()``."""
|
|
# Standard
|
|
import os
|
|
|
|
os.environ["DEVICE_TYPE"] = "musa"
|
|
stub = _StubTorch(
|
|
has_musa=True,
|
|
has_xpu=True,
|
|
has_hpu=True,
|
|
musa_available=True,
|
|
xpu_available=True,
|
|
hpu_available=True,
|
|
)
|
|
dev, name = _detect_with_stub(stub)
|
|
assert name == "musa"
|
|
assert dev is stub.musa
|
|
del os.environ["DEVICE_TYPE"]
|
|
|
|
|
|
def test_detect_device_falls_back_past_unavailable_musa() -> None:
|
|
"""Falls through MUSA when ``torch.musa.is_available()`` is False."""
|
|
# Standard
|
|
import os
|
|
|
|
os.environ["DEVICE_TYPE"] = "musa"
|
|
stub = _StubTorch(
|
|
has_musa=True,
|
|
musa_available=False,
|
|
)
|
|
_, name = _detect_with_stub(stub)
|
|
assert name == "cuda"
|
|
del os.environ["DEVICE_TYPE"]
|
|
|
|
|
|
def test_detect_device_cuda_fallback_when_no_alt_accelerator() -> None:
|
|
"""Default fallback is CUDA so existing CUDA tests/paths keep working."""
|
|
stub = _StubTorch()
|
|
dev, name = _detect_with_stub(stub)
|
|
assert name == "cuda"
|
|
assert dev is stub.cuda
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CreateGPUConnector: MUSA branch + device-scoped feature guards
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_create_gpu_connector_blending_rejected_on_musa(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""``enable_blending`` must fail fast on MUSA."""
|
|
_patch_device(monkeypatch, "musa")
|
|
config = _make_config(enable_blending=True, use_layerwise=True)
|
|
metadata = _make_metadata()
|
|
with pytest.raises(ValueError, match="enable_blending"):
|
|
CreateGPUConnector(config, metadata, EngineType.VLLM)
|
|
|
|
|
|
@pytest.mark.parametrize("device_type", ["musa", "hpu"])
|
|
def test_create_gpu_connector_blending_rejected_on_unsupported_devices(
|
|
monkeypatch: pytest.MonkeyPatch, device_type: str
|
|
) -> None:
|
|
"""The blending guard rejects devices without a blending connector."""
|
|
_patch_device(monkeypatch, device_type)
|
|
config = _make_config(enable_blending=True, use_layerwise=True)
|
|
metadata = _make_metadata()
|
|
with pytest.raises(ValueError, match="enable_blending"):
|
|
CreateGPUConnector(config, metadata, EngineType.VLLM)
|
|
|
|
|
|
def test_create_gpu_connector_v3_rejected_on_musa(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""``use_gpu_connector_v3`` must fail fast on MUSA."""
|
|
_patch_device(monkeypatch, "musa")
|
|
config = _make_config(use_gpu_connector_v3=True)
|
|
metadata = _make_metadata()
|
|
with pytest.raises(ValueError, match="use_gpu_connector_v3"):
|
|
CreateGPUConnector(config, metadata, EngineType.VLLM)
|
|
|
|
|
|
def test_create_gpu_connector_layerwise_rejected_on_hpu(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""HPU ships only ``VLLMPagedMemHPUConnectorV2`` (no layerwise variant).
|
|
|
|
Previously, ``use_layerwise=True`` on HPU silently fell through into
|
|
``VLLMPagedMemLayerwiseGPUConnector`` — the CUDA layerwise connector —
|
|
which then crashed on HPU tensors when constructing
|
|
``torch.cuda.Stream()``. The guard must reject this combination with a
|
|
clear error before any device-specific construction.
|
|
"""
|
|
_patch_device(monkeypatch, "hpu")
|
|
config = _make_config(use_layerwise=True)
|
|
metadata = _make_metadata()
|
|
with pytest.raises(ValueError) as exc:
|
|
CreateGPUConnector(config, metadata, EngineType.VLLM)
|
|
message = str(exc.value)
|
|
assert "use_layerwise" in message
|
|
assert "hpu" in message.lower()
|
|
|
|
|
|
def test_create_gpu_connector_musa_dispatches_to_musa_connector(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""``torch_device_type == 'musa'`` selects a MUSA connector class.
|
|
|
|
We do not need real MUSA hardware: the factory's ``torch.device`` call
|
|
is patched (PyTorch builds without ``torch_musa`` reject the 'musa'
|
|
string), and ``from_metadata`` is replaced on the MUSA connectors so
|
|
the assertion checks only the dispatch.
|
|
"""
|
|
_patch_device(monkeypatch, "musa")
|
|
# First Party
|
|
from lmcache.v1.gpu_connector import musa_connectors as musa_mod
|
|
|
|
monkeypatch.setattr(
|
|
gpu_connector_module.torch, "device", lambda *_a, **_kw: "musa:0"
|
|
)
|
|
|
|
sentinel_v2 = object()
|
|
sentinel_layer = object()
|
|
monkeypatch.setattr(
|
|
musa_mod.VLLMPagedMemMUSAConnectorV2,
|
|
"from_metadata",
|
|
classmethod(lambda cls, *a, **kw: sentinel_v2),
|
|
)
|
|
monkeypatch.setattr(
|
|
musa_mod.VLLMPagedMemLayerwiseMUSAConnector,
|
|
"from_metadata",
|
|
classmethod(lambda cls, *a, **kw: sentinel_layer),
|
|
)
|
|
|
|
metadata = _make_metadata()
|
|
assert (
|
|
CreateGPUConnector(_make_config(use_layerwise=False), metadata, EngineType.VLLM)
|
|
is sentinel_v2
|
|
)
|
|
assert (
|
|
CreateGPUConnector(_make_config(use_layerwise=True), metadata, EngineType.VLLM)
|
|
is sentinel_layer
|
|
)
|
|
|
|
|
|
def test_create_gpu_connector_rejects_sglang_on_musa(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Stage2 does not expose SGLang MUSA connector dispatch."""
|
|
_patch_device(monkeypatch, "musa")
|
|
with pytest.raises(ValueError, match="SGLang on MUSA"):
|
|
CreateGPUConnector(_make_config(), _make_metadata(), EngineType.SGLANG)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module import sanity
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_lmcache_exports_torch_dev_and_torch_device_type() -> None:
|
|
"""The contract used by every accelerator-aware module is the
|
|
``torch_dev`` / ``torch_device_type`` pair exported from ``lmcache``.
|
|
|
|
Re-import to defeat any per-test monkeypatching above.
|
|
"""
|
|
importlib.reload(lmc)
|
|
assert hasattr(lmc, "torch_dev")
|
|
assert isinstance(lmc.torch_device_type, str)
|
|
assert lmc.torch_device_type in {"cuda", "musa", "xpu", "hpu", "cpu"}
|