727 lines
26 KiB
Python
727 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import imagefind.accelerator as accelerator_module
|
|
import imagefind.ai as ai_module
|
|
import imagefind.main as main_module
|
|
import pytest
|
|
from imagefind.accelerator import AcceleratorService
|
|
from imagefind.ai import EmbeddingService, ModelUnavailable, _OpenVINOClipImageEncoder, _OpenVINOOrtSession
|
|
from imagefind.config import Settings
|
|
from imagefind.media import MediaService
|
|
|
|
|
|
def _openvino(monkeypatch, devices: list[str]):
|
|
class Core:
|
|
available_devices = devices
|
|
|
|
@staticmethod
|
|
def get_property(device, _name):
|
|
return {"CPU": "Intel CPU", "GPU.0": "Intel UHD Graphics"}.get(device, device)
|
|
|
|
monkeypatch.setitem(sys.modules, "openvino", types.SimpleNamespace(Core=Core))
|
|
|
|
|
|
def _render_nodes(monkeypatch, nodes: list[Path], accessible: bool = True):
|
|
original_glob = Path.glob
|
|
original_access = os.access
|
|
|
|
def fake_glob(path: Path, pattern: str):
|
|
if path == Path("/dev/dri") and pattern == "renderD*":
|
|
return iter(nodes)
|
|
return original_glob(path, pattern)
|
|
|
|
monkeypatch.setattr(Path, "glob", fake_glob)
|
|
monkeypatch.setattr(
|
|
accelerator_module.os,
|
|
"access",
|
|
lambda path, mode: accessible if str(path).startswith("/dev/dri/renderD") else original_access(path, mode),
|
|
)
|
|
|
|
|
|
def test_accelerator_prefers_gpu_and_falls_back_per_component(monkeypatch, tmp_path: Path):
|
|
_openvino(monkeypatch, ["CPU", "GPU.0"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")])
|
|
service = AcceleratorService(Settings(data_dir=tmp_path, ai_cpu_threads=3))
|
|
|
|
assert service.gpu_device == "GPU.0"
|
|
assert service.device_for("visual") == "GPU.0"
|
|
assert service.status()["device_names"]["GPU.0"] == "Intel UHD Graphics"
|
|
service.mark_ready("visual", "GPU.0")
|
|
assert service.fall_back("visual", RuntimeError("secret driver detail")) is True
|
|
assert service.device_for("visual") == "CPU"
|
|
assert service.device_for("ocr") == "GPU.0"
|
|
assert service.fall_back("visual", RuntimeError("do not retry")) is False
|
|
visual = service.status()["components"]["visual"]
|
|
assert visual["state"] == "fallback"
|
|
assert "secret driver detail" not in visual["fallback_reason"]
|
|
assert visual["requested_device"] == "GPU.0"
|
|
assert visual["actual_device"] == "CPU"
|
|
assert visual["failure_stage"] == "inference"
|
|
assert service.ov_config("CPU") == {"INFERENCE_NUM_THREADS": 3, "PERFORMANCE_HINT": "LATENCY"}
|
|
gpu_config = service.ov_config("GPU.0")
|
|
assert gpu_config["PERFORMANCE_HINT"] == "LATENCY"
|
|
assert gpu_config["NUM_STREAMS"] == "1"
|
|
assert gpu_config["CACHE_DIR"].endswith("openvino-cache")
|
|
|
|
service.reset("visual")
|
|
visual = service.status()["components"]["visual"]
|
|
assert visual["state"] == "not_loaded"
|
|
assert visual["device"] is None
|
|
assert visual["requested_device"] == "GPU.0"
|
|
assert visual["actual_device"] == "CPU"
|
|
assert visual["execution_devices"] == ["CPU"]
|
|
assert visual["last_verified_at"] is not None
|
|
assert visual["failure_stage"] is None
|
|
assert visual["fallback_reason"] is None
|
|
assert service.device_for("visual") == "GPU.0"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mode", "hint", "streams", "batch_size"),
|
|
[("low", "LATENCY", "1", 1), ("balanced", "THROUGHPUT", "2", 2), ("throughput", "THROUGHPUT", "3", 4)],
|
|
)
|
|
def test_gpu_profiles_bound_streams_and_batches(monkeypatch, tmp_path: Path, mode, hint, streams, batch_size):
|
|
_openvino(monkeypatch, ["CPU", "GPU.0"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")])
|
|
service = AcceleratorService(Settings(data_dir=tmp_path, ai_gpu_mode=mode))
|
|
config = service.ov_config("GPU.0")
|
|
assert config["PERFORMANCE_HINT"] == hint
|
|
assert config["NUM_STREAMS"] == streams
|
|
assert service.gpu_profile()["batch_size"] == batch_size
|
|
assert service.settings.ai_cpu_threads == 1
|
|
|
|
|
|
def test_accelerator_reset_without_prior_inference_remains_never_loaded(monkeypatch, tmp_path: Path):
|
|
_openvino(monkeypatch, ["CPU", "GPU.0"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")])
|
|
service = AcceleratorService(Settings(data_dir=tmp_path))
|
|
|
|
service.reset("faces")
|
|
|
|
faces = service.status()["components"]["faces"]
|
|
assert faces["state"] == "not_loaded"
|
|
assert faces["requested_device"] is None
|
|
assert faces["actual_device"] is None
|
|
assert faces["execution_devices"] == []
|
|
assert faces["last_verified_at"] is None
|
|
|
|
|
|
def test_accelerator_cpu_verification_is_transient_but_can_become_fallback(
|
|
monkeypatch,
|
|
tmp_path: Path,
|
|
):
|
|
_openvino(monkeypatch, ["CPU", "GPU.0"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")])
|
|
service = AcceleratorService(Settings(data_dir=tmp_path))
|
|
|
|
service.mark_ready("audio", "GPU.0", ["GPU.0"])
|
|
service.mark_verifying_cpu("audio")
|
|
|
|
verifying = service.status()["components"]["audio"]
|
|
assert verifying["state"] == "verifying_cpu"
|
|
assert verifying["actual_device"] == "CPU"
|
|
assert service.device_for("audio") == "GPU.0"
|
|
assert service.fall_back("audio", "CPU sample contained speech", stage="empty_result") is True
|
|
|
|
fallback = service.status()["components"]["audio"]
|
|
assert fallback["state"] == "fallback"
|
|
assert fallback["actual_device"] == "CPU"
|
|
assert service.device_for("audio") == "CPU"
|
|
|
|
|
|
def test_audio_transient_device_failures_only_open_circuit_after_three_tasks(
|
|
monkeypatch,
|
|
tmp_path: Path,
|
|
):
|
|
_openvino(monkeypatch, ["CPU", "GPU.0"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")])
|
|
service = AcceleratorService(Settings(data_dir=tmp_path))
|
|
|
|
assert service.record_transient_failure("audio", "stall one", stage="inference_stall") is False
|
|
assert service.device_for("audio") == "GPU.0"
|
|
assert service.record_transient_failure("audio", "stall two", stage="inference_stall") is False
|
|
assert service.device_for("audio") == "GPU.0"
|
|
assert service.record_transient_failure("audio", "stall three", stage="inference_stall") is True
|
|
|
|
status = service.status()["components"]["audio"]
|
|
assert status["state"] == "fallback"
|
|
assert status["fallback_scope"] == "component"
|
|
assert status["circuit_state"] == "open"
|
|
assert status["failure_count"] == 3
|
|
assert status["retry_at"]
|
|
assert service.device_for("audio") == "CPU"
|
|
|
|
service.mark_inference_success("audio", "GPU.0", ["GPU.0"])
|
|
status = service.status()["components"]["audio"]
|
|
assert status["circuit_state"] == "closed"
|
|
assert status["failure_count"] == 0
|
|
|
|
|
|
def test_accelerator_reports_render_permission_problem(monkeypatch, tmp_path: Path):
|
|
_openvino(monkeypatch, ["CPU"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")], accessible=False)
|
|
service = AcceleratorService(Settings(data_dir=tmp_path))
|
|
|
|
assert service.gpu_device is None
|
|
assert service.status()["render_device"] == {"available": True, "accessible": False, "count": 1}
|
|
assert service.status()["unavailable_reason"] == "Intel render 设备权限不足"
|
|
|
|
|
|
def test_gpu_sysfs_metrics_need_two_samples_and_do_not_invent_shared_vram(
|
|
monkeypatch,
|
|
tmp_path: Path,
|
|
):
|
|
_openvino(monkeypatch, ["CPU", "GPU.0"])
|
|
_render_nodes(monkeypatch, [Path("/dev/dri/renderD128")])
|
|
service = AcceleratorService(Settings(data_dir=tmp_path))
|
|
busy_path = Path("/sys/class/drm/card0/engine/rcs0/busy")
|
|
original_glob = Path.glob
|
|
values = iter((1_000_000_000, 1_500_000_000))
|
|
|
|
def fake_glob(path: Path, pattern: str):
|
|
if path == Path("/sys/class/drm") and pattern == "card*/engine/*/busy":
|
|
return iter((busy_path,))
|
|
if path == Path("/sys/class/drm") and pattern == "card*/device":
|
|
return iter(())
|
|
return original_glob(path, pattern)
|
|
|
|
original_read_text = Path.read_text
|
|
monkeypatch.setattr(Path, "glob", fake_glob)
|
|
monkeypatch.setattr(
|
|
Path,
|
|
"read_text",
|
|
lambda path, **kwargs: str(next(values))
|
|
if path == busy_path
|
|
else original_read_text(path, **kwargs),
|
|
)
|
|
clock = iter((10.0, 11.0))
|
|
monkeypatch.setattr(accelerator_module.time, "monotonic", lambda: next(clock))
|
|
|
|
first = service.hardware_metrics()
|
|
second = service.hardware_metrics()
|
|
assert first["supported"] is True
|
|
assert first["utilization_percent"] is None
|
|
assert second["utilization_percent"] == 50.0
|
|
assert second["engines"] == {"card0/rcs0": 50.0}
|
|
assert second["memory_supported"] is False
|
|
assert second["memory_total_bytes"] is None
|
|
|
|
|
|
def test_visual_models_refuse_runtime_export_and_require_persisted_ir(tmp_path: Path):
|
|
settings = Settings(data_dir=tmp_path)
|
|
for name in ("image", "text"):
|
|
root = settings.models_dir / "visual" / name
|
|
root.mkdir(parents=True)
|
|
(root / "modules.json").write_text('[{"idx":0,"path":"","type":"Transformer"}]')
|
|
|
|
class Accelerator:
|
|
@staticmethod
|
|
def ov_config(_device):
|
|
return {"PERFORMANCE_HINT": "THROUGHPUT"}
|
|
|
|
@staticmethod
|
|
def mark_ready(_component, _device):
|
|
pass
|
|
|
|
calls = []
|
|
|
|
class Model:
|
|
def __init__(self, path, **kwargs):
|
|
calls.append((path, kwargs))
|
|
|
|
service = EmbeddingService(settings, Accelerator())
|
|
with pytest.raises(ModelUnavailable, match="OpenVINO 模型不完整"):
|
|
service._build_models(Model, "openvino", "GPU.0")
|
|
assert calls == []
|
|
|
|
|
|
@pytest.mark.parametrize("text_module", ["", "0_Transformer"])
|
|
def test_visual_models_reuse_existing_openvino_exports(
|
|
tmp_path: Path, monkeypatch, text_module: str
|
|
):
|
|
settings = Settings(data_dir=tmp_path)
|
|
for name in ("image", "text"):
|
|
root = settings.models_dir / "visual" / name
|
|
module = root / text_module if name == "text" and text_module else root
|
|
export = module / "openvino"
|
|
export.mkdir(parents=True)
|
|
module_type = "Transformer" if name == "text" else "CLIPModel"
|
|
module_path = text_module if name == "text" else ""
|
|
(root / "modules.json").write_text(
|
|
f'['
|
|
f'{{"idx":0,"path":"{module_path}","type":"{module_type}"}}'
|
|
f']'
|
|
)
|
|
(module / "config.json").write_text("{}")
|
|
(export / "openvino_model.xml").write_text("<xml/>")
|
|
(export / "openvino_model.bin").write_bytes(b"model")
|
|
(export / "config.json").write_text("{}")
|
|
|
|
class Accelerator:
|
|
@staticmethod
|
|
def ov_config(_device):
|
|
return {"PERFORMANCE_HINT": "THROUGHPUT"}
|
|
|
|
@staticmethod
|
|
def execution_devices_from(*_models):
|
|
return ["GPU.0"]
|
|
|
|
@staticmethod
|
|
def mark_ready(_component, _device, _execution_devices):
|
|
pass
|
|
|
|
text_calls = []
|
|
image_calls = []
|
|
|
|
class ImageModel:
|
|
def __init__(self, path, device, ov_config):
|
|
image_calls.append((path, device, ov_config))
|
|
|
|
class Model:
|
|
def __init__(self, path, **kwargs):
|
|
text_calls.append((path, kwargs))
|
|
|
|
monkeypatch.setattr(ai_module, "_OpenVINOClipImageEncoder", ImageModel)
|
|
EmbeddingService(settings, Accelerator())._build_models(Model, "openvino", "GPU.0")
|
|
|
|
assert len(image_calls) == 1
|
|
assert Path(image_calls[0][0]).is_absolute()
|
|
assert image_calls[0][1:] == ("GPU", {"PERFORMANCE_HINT": "THROUGHPUT"})
|
|
assert len(text_calls) == 1
|
|
assert Path(text_calls[0][0]).is_absolute()
|
|
assert text_calls[0][1]["local_files_only"] is True
|
|
assert text_calls[0][1]["model_kwargs"] == {
|
|
"device": "GPU",
|
|
"ov_config": {"PERFORMANCE_HINT": "THROUGHPUT"},
|
|
"export": False,
|
|
"file_name": "openvino_model.xml",
|
|
"subfolder": "openvino",
|
|
}
|
|
|
|
|
|
def test_clip_image_encoder_uses_image_aware_openvino_model(monkeypatch, tmp_path: Path):
|
|
root = tmp_path / "image"
|
|
export = root / "openvino"
|
|
export.mkdir(parents=True)
|
|
processor_root = root / "0_CLIPModel"
|
|
processor_root.mkdir()
|
|
(root / "modules.json").write_text(
|
|
'[{"idx":0,"path":"0_CLIPModel","type":"sentence_transformers.models.CLIPModel"}]'
|
|
)
|
|
(processor_root / "config.json").write_text("{}")
|
|
(export / "openvino_model.xml").write_text("<xml/>")
|
|
(export / "openvino_model.bin").write_bytes(b"model")
|
|
(export / "config.json").write_text("{}")
|
|
calls = {}
|
|
|
|
class Matrix:
|
|
def __init__(self, rows):
|
|
self.rows = rows
|
|
|
|
def __truediv__(self, denominators):
|
|
return Matrix(
|
|
[[value / denominators[index][0] for value in row] for index, row in enumerate(self.rows)]
|
|
)
|
|
|
|
def tolist(self):
|
|
return self.rows
|
|
|
|
numpy = types.ModuleType("numpy")
|
|
numpy.float32 = "float32"
|
|
numpy.asarray = lambda value, dtype=None: Matrix(value)
|
|
numpy.maximum = lambda values, _minimum: values
|
|
numpy.finfo = lambda _dtype: types.SimpleNamespace(eps=1e-7)
|
|
numpy.linalg = types.SimpleNamespace(
|
|
norm=lambda values, axis, keepdims: [
|
|
[sum(item * item for item in row) ** 0.5] for row in values.rows
|
|
]
|
|
)
|
|
|
|
class OpenVINOModel:
|
|
@classmethod
|
|
def from_pretrained(cls, _path, **_kwargs):
|
|
raise AssertionError("public loader must not perform Hugging Face library inference")
|
|
|
|
@classmethod
|
|
def _from_pretrained(cls, path, **kwargs):
|
|
calls["model"] = (path, kwargs)
|
|
return cls()
|
|
|
|
def __call__(self, **inputs):
|
|
calls["inputs"] = inputs
|
|
return types.SimpleNamespace(image_embeds=[[3.0, 4.0]])
|
|
|
|
class Processor:
|
|
@classmethod
|
|
def from_pretrained(cls, path, **kwargs):
|
|
calls["processor"] = (path, kwargs)
|
|
return cls()
|
|
|
|
def __call__(self, **kwargs):
|
|
calls["processor_inputs"] = kwargs
|
|
return {"input_ids": [[1]], "pixel_values": [[0.0]]}
|
|
|
|
class Config:
|
|
@classmethod
|
|
def from_pretrained(cls, path, **kwargs):
|
|
calls["config"] = (path, kwargs)
|
|
return cls()
|
|
|
|
optimum = types.ModuleType("optimum")
|
|
optimum_intel = types.ModuleType("optimum.intel")
|
|
optimum_openvino = types.ModuleType("optimum.intel.openvino")
|
|
optimum_openvino.OVModelForZeroShotImageClassification = OpenVINOModel
|
|
transformers = types.ModuleType("transformers")
|
|
transformers.AutoConfig = Config
|
|
transformers.AutoProcessor = Processor
|
|
for name, module in {
|
|
"optimum": optimum,
|
|
"optimum.intel": optimum_intel,
|
|
"optimum.intel.openvino": optimum_openvino,
|
|
"transformers": transformers,
|
|
"numpy": numpy,
|
|
}.items():
|
|
monkeypatch.setitem(sys.modules, name, module)
|
|
|
|
encoder = _OpenVINOClipImageEncoder(root, "GPU.0", {"NUM_STREAMS": "1"})
|
|
vectors = encoder.encode([object()])
|
|
|
|
assert calls["model"] == (
|
|
str(export.resolve()),
|
|
{
|
|
"config": calls["model"][1]["config"],
|
|
"file_name": "openvino_model.xml",
|
|
"local_files_only": True,
|
|
"device": "GPU.0",
|
|
"ov_config": {"NUM_STREAMS": "1"},
|
|
},
|
|
)
|
|
assert isinstance(calls["model"][1]["config"], Config)
|
|
assert calls["config"] == (str(processor_root.resolve()), {"local_files_only": True})
|
|
assert calls["processor"] == (str(processor_root.resolve()), {"local_files_only": True})
|
|
assert calls["processor_inputs"]["text"] == [""]
|
|
assert set(calls["inputs"]) == {"input_ids", "pixel_values"}
|
|
assert [round(value, 6) for value in vectors.tolist()[0]] == [0.6, 0.8]
|
|
|
|
|
|
def test_visual_text_encoder_bypasses_optimum_hub_inference(
|
|
tmp_path: Path, monkeypatch
|
|
):
|
|
settings = Settings(data_dir=tmp_path)
|
|
for name in ("image", "text"):
|
|
root = settings.models_dir / "visual" / name
|
|
export = root / "openvino"
|
|
export.mkdir(parents=True)
|
|
module_type = "CLIPModel" if name == "image" else "Transformer"
|
|
(root / "modules.json").write_text(
|
|
f'[' f'{{"idx":0,"path":"","type":"{module_type}"}}' f']'
|
|
)
|
|
(root / "config.json").write_text("{}")
|
|
(export / "openvino_model.xml").write_text("<xml/>")
|
|
(export / "openvino_model.bin").write_bytes(b"model")
|
|
(export / "config.json").write_text("{}")
|
|
|
|
calls = {}
|
|
|
|
class OpenVINOTextModel:
|
|
@classmethod
|
|
def from_pretrained(cls, _path, **_kwargs):
|
|
raise AssertionError("public Optimum loader must not infer a Hub library")
|
|
|
|
@classmethod
|
|
def _from_pretrained(cls, **kwargs):
|
|
calls["local"] = kwargs
|
|
return object()
|
|
|
|
class SentenceTransformerModel:
|
|
__module__ = "sentence_transformers.sentence_transformer.model"
|
|
|
|
def __init__(self, path, **kwargs):
|
|
calls["wrapper"] = (path, kwargs)
|
|
options = kwargs["model_kwargs"]
|
|
self.model = OpenVINOTextModel.from_pretrained(
|
|
path,
|
|
config="local-config",
|
|
export=options["export"],
|
|
file_name=options["file_name"],
|
|
subfolder=options["subfolder"],
|
|
local_files_only=kwargs["local_files_only"],
|
|
device=options["device"],
|
|
ov_config=options["ov_config"],
|
|
)
|
|
|
|
optimum = types.ModuleType("optimum")
|
|
optimum_intel = types.ModuleType("optimum.intel")
|
|
optimum_openvino = types.ModuleType("optimum.intel.openvino")
|
|
optimum_openvino.OVModelForFeatureExtraction = OpenVINOTextModel
|
|
for name, module in {
|
|
"optimum": optimum,
|
|
"optimum.intel": optimum_intel,
|
|
"optimum.intel.openvino": optimum_openvino,
|
|
}.items():
|
|
monkeypatch.setitem(sys.modules, name, module)
|
|
|
|
class Accelerator:
|
|
@staticmethod
|
|
def ov_config(_device):
|
|
return {"NUM_STREAMS": "1"}
|
|
|
|
@staticmethod
|
|
def execution_devices_from(*_models):
|
|
return ["GPU"]
|
|
|
|
@staticmethod
|
|
def mark_ready(*_args):
|
|
pass
|
|
|
|
monkeypatch.setattr(ai_module, "_OpenVINOClipImageEncoder", lambda *_args: object())
|
|
EmbeddingService(settings, Accelerator())._build_models(
|
|
SentenceTransformerModel, "openvino", "GPU"
|
|
)
|
|
|
|
assert calls["local"] == {
|
|
"model_id": str((settings.models_dir / "visual" / "text").resolve()),
|
|
"config": "local-config",
|
|
"file_name": "openvino_model.xml",
|
|
"subfolder": "openvino",
|
|
"local_files_only": True,
|
|
"device": "GPU",
|
|
"ov_config": {"NUM_STREAMS": "1"},
|
|
}
|
|
with pytest.raises(AssertionError, match="public Optimum loader"):
|
|
OpenVINOTextModel.from_pretrained("/vol1/imagefind/models/visual/text")
|
|
|
|
|
|
def test_hugging_face_validation_errors_are_not_gpu_device_errors():
|
|
error_type = type("HFValidationError", (ValueError,), {"__module__": "huggingface_hub.errors"})
|
|
assert AcceleratorService.is_device_error(error_type("invalid repo id")) is False
|
|
assert AcceleratorService.is_device_error(ModelUnavailable("OpenVINO IR 尚未准备完成")) is False
|
|
assert AcceleratorService.is_device_error(RuntimeError("OpenVINO GPU compile_model failed")) is True
|
|
|
|
|
|
def test_execution_devices_are_read_from_compiled_wrappers():
|
|
class Compiled:
|
|
@staticmethod
|
|
def get_property(name):
|
|
assert name == "EXECUTION_DEVICES"
|
|
return ["GPU.0"]
|
|
|
|
class Request:
|
|
@staticmethod
|
|
def get_compiled_model():
|
|
return Compiled()
|
|
|
|
wrapper = types.SimpleNamespace(encoder=types.SimpleNamespace(request=Request()))
|
|
assert AcceleratorService.execution_devices_from(wrapper) == ["GPU.0"]
|
|
|
|
|
|
def test_execution_device_discovery_ignores_unsupported_gpu_properties():
|
|
class UnsupportedProperties:
|
|
@staticmethod
|
|
def get_property(_name):
|
|
raise ValueError("property is not supported by this GPU wrapper")
|
|
|
|
@staticmethod
|
|
def get_compiled_model():
|
|
raise ValueError("compiled model is exposed by a sibling request")
|
|
|
|
class Compiled:
|
|
@staticmethod
|
|
def get_property(name):
|
|
assert name == "EXECUTION_DEVICES"
|
|
return ["GPU.0"]
|
|
|
|
wrapper = types.SimpleNamespace(
|
|
model=UnsupportedProperties(),
|
|
request=Compiled(),
|
|
)
|
|
|
|
assert AcceleratorService.execution_devices_from(wrapper) == ["GPU.0"]
|
|
|
|
|
|
def test_openvino_value_errors_retry_on_cpu_but_model_layout_errors_do_not():
|
|
assert AcceleratorService.is_device_error(ValueError("GPU plugin property failed")) is True
|
|
assert AcceleratorService.is_device_error(ValueError("modules.json is invalid")) is False
|
|
|
|
|
|
def test_visual_gpu_value_error_retries_complete_model_on_cpu(tmp_path: Path, monkeypatch):
|
|
settings = Settings(data_dir=tmp_path)
|
|
for name in ("image", "text"):
|
|
root = settings.models_dir / "visual" / name
|
|
export = root / "openvino"
|
|
export.mkdir(parents=True)
|
|
(root / "modules.json").write_text('[{"idx":0,"path":"","type":"Transformer"}]')
|
|
(root / "config.json").write_text("{}")
|
|
(export / "openvino_model.xml").write_text("<xml/>")
|
|
(export / "openvino_model.bin").write_bytes(b"model")
|
|
(export / "config.json").write_text("{}")
|
|
|
|
calls = []
|
|
unavailable = []
|
|
|
|
class Accelerator:
|
|
def __init__(self):
|
|
self.fallback = False
|
|
|
|
def device_for(self, _component):
|
|
return "CPU" if self.fallback else "GPU.0"
|
|
|
|
@staticmethod
|
|
def is_device_error(error):
|
|
return isinstance(error, ValueError)
|
|
|
|
def fall_back(self, _component, _error, stage):
|
|
assert stage == "image_encoder_compile"
|
|
self.fallback = True
|
|
return True
|
|
|
|
@staticmethod
|
|
def ov_config(device):
|
|
return {"device_config": device}
|
|
|
|
@staticmethod
|
|
def execution_devices_from(*_models):
|
|
return ["CPU"]
|
|
|
|
@staticmethod
|
|
def mark_ready(component, device, execution_devices):
|
|
calls.append((component, device, execution_devices))
|
|
|
|
@staticmethod
|
|
def mark_unavailable(component, error, stage):
|
|
unavailable.append((component, error, stage))
|
|
|
|
class ImageModel:
|
|
def __init__(self, _path, device, _ov_config):
|
|
if device == "GPU":
|
|
raise ValueError("Intel GPU property rejected by wrapper")
|
|
|
|
class TextModel:
|
|
def __init__(self, _path, **kwargs):
|
|
assert kwargs["model_kwargs"]["device"] == "CPU"
|
|
|
|
sentence_transformers = types.ModuleType("sentence_transformers")
|
|
sentence_transformers.SentenceTransformer = TextModel
|
|
monkeypatch.setitem(sys.modules, "sentence_transformers", sentence_transformers)
|
|
monkeypatch.setattr(ai_module, "_OpenVINOClipImageEncoder", ImageModel)
|
|
|
|
service = EmbeddingService(settings, Accelerator())
|
|
service._load()
|
|
|
|
assert service._image_model is not None
|
|
assert service._text_model is not None
|
|
assert calls == [("visual", "CPU", ["CPU"])]
|
|
assert unavailable == []
|
|
|
|
|
|
def test_openvino_ocr_adapter_compiles_requested_device(monkeypatch):
|
|
captured = {}
|
|
|
|
class Metadata:
|
|
value = "a\nb"
|
|
|
|
class Model:
|
|
@staticmethod
|
|
def get_rt_info():
|
|
return {"framework": {"character": Metadata()}}
|
|
|
|
class Compiled:
|
|
@staticmethod
|
|
def output(_index):
|
|
return "output"
|
|
|
|
def __call__(self, values):
|
|
captured["values"] = values
|
|
return {"output": "result"}
|
|
|
|
class Core:
|
|
@staticmethod
|
|
def read_model(path):
|
|
captured["model"] = path
|
|
return Model()
|
|
|
|
@staticmethod
|
|
def compile_model(model, device, config):
|
|
captured.update(compiled_model=model, device=device, config=config)
|
|
return Compiled()
|
|
|
|
monkeypatch.setitem(sys.modules, "openvino", types.SimpleNamespace(Core=Core))
|
|
session = _OpenVINOOrtSession("ocr.onnx", "GPU.0", {"PERFORMANCE_HINT": "THROUGHPUT"})
|
|
|
|
assert session("pixels") == ["result"]
|
|
assert session.have_key() is True
|
|
assert session.get_character_list() == ["a", "b"]
|
|
assert captured["model"] == "ocr.onnx"
|
|
assert isinstance(captured["compiled_model"], Model)
|
|
assert captured["device"] == "GPU.0"
|
|
assert captured["config"] == {"PERFORMANCE_HINT": "THROUGHPUT"}
|
|
assert captured["values"] == ["pixels"]
|
|
|
|
|
|
def test_remote_media_uses_runtime_internal_port(tmp_path: Path):
|
|
settings = Settings(data_dir=tmp_path, port=8765, internal_media_port=43123)
|
|
media = MediaService(settings).input_for({"id": "remote-video", "source_kind": "webdav"})
|
|
|
|
assert media.value == "http://127.0.0.1:43123/api/internal/remote/remote-video"
|
|
assert media.headers and media.headers.startswith("X-ImageFind-Internal: ")
|
|
assert media.headers.endswith("\r\n")
|
|
|
|
|
|
def test_gateway_only_serve_creates_private_random_tcp_listener(monkeypatch, tmp_path: Path):
|
|
gateway_socket = tmp_path / "imagefind.sock"
|
|
settings = Settings(
|
|
data_dir=tmp_path / "data",
|
|
gateway_socket=gateway_socket,
|
|
direct_access=False,
|
|
host="0.0.0.0",
|
|
port=8765,
|
|
internal_media_port=0,
|
|
)
|
|
captured = {}
|
|
|
|
class Listener:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def getsockname(self):
|
|
return self.name
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
class Config:
|
|
def __init__(self, app, **kwargs):
|
|
captured.update(app=app, config=kwargs)
|
|
|
|
class Server:
|
|
def __init__(self, _config):
|
|
pass
|
|
|
|
@staticmethod
|
|
def run(sockets):
|
|
captured["listeners"] = [listener.getsockname() for listener in sockets]
|
|
|
|
monkeypatch.setattr(main_module, "create_app", lambda current: ("app", current.internal_media_port))
|
|
monkeypatch.setattr(
|
|
main_module,
|
|
"_tcp_socket",
|
|
lambda host, port: captured.setdefault("tcp_request", (host, port)) and Listener((host, 43123)),
|
|
)
|
|
monkeypatch.setattr(main_module, "_unix_socket", lambda path, _mode: Listener(str(path)))
|
|
monkeypatch.setitem(sys.modules, "uvicorn", types.SimpleNamespace(Config=Config, Server=Server))
|
|
|
|
main_module.serve(settings)
|
|
|
|
assert captured["tcp_request"] == ("127.0.0.1", 0)
|
|
assert settings.internal_media_port == 43123
|
|
assert captured["app"] == ("app", settings.internal_media_port)
|
|
assert captured["listeners"][0] == ("127.0.0.1", settings.internal_media_port)
|
|
assert captured["listeners"][1] == str(gateway_socket)
|
|
assert all(listener != ("0.0.0.0", 8765) for listener in captured["listeners"])
|
|
assert not gateway_socket.exists()
|