e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
525 lines
19 KiB
Python
525 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Sandbox test for multi-GPU selection logic.
|
|
|
|
Tests GPU selection, memory estimation, and device_map logic in
|
|
isolation. Runs on Linux, macOS, and Windows without real GPUs -- all
|
|
hardware calls are mocked.
|
|
|
|
Usage:
|
|
python -m pytest studio/backend/tests/test_gpu_selection_sandbox.py -v
|
|
# or directly:
|
|
python studio/backend/tests/test_gpu_selection_sandbox.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
# Ensure backend is on sys.path.
|
|
_backend_root = Path(__file__).resolve().parent.parent
|
|
if str(_backend_root) not in sys.path:
|
|
sys.path.insert(0, str(_backend_root))
|
|
|
|
|
|
def _make_fake_config(
|
|
vocab_size = 32000,
|
|
hidden_size = 4096,
|
|
intermediate_size = 11008,
|
|
num_hidden_layers = 32,
|
|
num_attention_heads = 32,
|
|
num_key_value_heads = 8,
|
|
tie_word_embeddings = False,
|
|
):
|
|
"""Fake HF config-like object for estimation tests."""
|
|
from types import SimpleNamespace
|
|
return SimpleNamespace(
|
|
vocab_size = vocab_size,
|
|
hidden_size = hidden_size,
|
|
intermediate_size = intermediate_size,
|
|
num_hidden_layers = num_hidden_layers,
|
|
num_attention_heads = num_attention_heads,
|
|
num_key_value_heads = num_key_value_heads,
|
|
tie_word_embeddings = tie_word_embeddings,
|
|
)
|
|
|
|
|
|
class TestEstimateFP16ModelSizeFromConfig(unittest.TestCase):
|
|
"""Config-based model size estimation."""
|
|
|
|
def test_llama_8b_size_reasonable(self):
|
|
from utils.hardware.hardware import _estimate_fp16_model_size_bytes_from_config
|
|
|
|
config = _make_fake_config(
|
|
vocab_size = 128256,
|
|
hidden_size = 4096,
|
|
intermediate_size = 14336,
|
|
num_hidden_layers = 32,
|
|
num_attention_heads = 32,
|
|
num_key_value_heads = 8,
|
|
tie_word_embeddings = False,
|
|
)
|
|
size = _estimate_fp16_model_size_bytes_from_config(config)
|
|
self.assertIsNotNone(size)
|
|
size_gb = size / (1024**3)
|
|
# Llama 3.1 8B should be ~15GB in fp16
|
|
self.assertGreater(size_gb, 12)
|
|
self.assertLess(size_gb, 20)
|
|
|
|
def test_small_model(self):
|
|
from utils.hardware.hardware import _estimate_fp16_model_size_bytes_from_config
|
|
|
|
config = _make_fake_config(
|
|
vocab_size = 32000,
|
|
hidden_size = 2048,
|
|
intermediate_size = 5504,
|
|
num_hidden_layers = 22,
|
|
num_attention_heads = 32,
|
|
num_key_value_heads = 4,
|
|
)
|
|
size = _estimate_fp16_model_size_bytes_from_config(config)
|
|
self.assertIsNotNone(size)
|
|
size_gb = size / (1024**3)
|
|
# ~1B model should be ~2GB in fp16
|
|
self.assertGreater(size_gb, 1)
|
|
self.assertLess(size_gb, 5)
|
|
|
|
def test_returns_none_for_incomplete_config(self):
|
|
from utils.hardware.hardware import _estimate_fp16_model_size_bytes_from_config
|
|
from types import SimpleNamespace
|
|
|
|
config = SimpleNamespace(vocab_size = 32000) # most fields missing
|
|
size = _estimate_fp16_model_size_bytes_from_config(config)
|
|
self.assertIsNone(size)
|
|
|
|
def test_moe_model(self):
|
|
from utils.hardware.hardware import _estimate_fp16_model_size_bytes_from_config
|
|
from types import SimpleNamespace
|
|
|
|
config = SimpleNamespace(
|
|
vocab_size = 152064,
|
|
hidden_size = 3584,
|
|
intermediate_size = 18944,
|
|
num_hidden_layers = 28,
|
|
num_attention_heads = 28,
|
|
num_key_value_heads = 4,
|
|
tie_word_embeddings = False,
|
|
num_local_experts = 64,
|
|
moe_intermediate_size = 2560,
|
|
)
|
|
size = _estimate_fp16_model_size_bytes_from_config(config)
|
|
self.assertIsNotNone(size)
|
|
size_gb = size / (1024**3)
|
|
# MoE model with 64 experts should be large
|
|
self.assertGreater(size_gb, 50)
|
|
|
|
|
|
class TestEstimateRequiredModelMemory(unittest.TestCase):
|
|
"""Memory requirement estimation."""
|
|
|
|
def test_inference_fp16_uses_1_3x(self):
|
|
from utils.hardware.hardware import estimate_required_model_memory_gb
|
|
with patch(
|
|
"utils.hardware.hardware.estimate_fp16_model_size_bytes",
|
|
return_value = (10 * (1024**3), "config"), # 10GB model
|
|
):
|
|
required, meta = estimate_required_model_memory_gb(
|
|
"test/model",
|
|
training_type = None, # inference
|
|
load_in_4bit = False,
|
|
)
|
|
self.assertIsNotNone(required)
|
|
self.assertAlmostEqual(required, 13.0, places = 0)
|
|
self.assertEqual(meta["mode"], "inference")
|
|
|
|
def test_inference_4bit_uses_reduced_estimate(self):
|
|
from utils.hardware.hardware import estimate_required_model_memory_gb
|
|
with patch(
|
|
"utils.hardware.hardware.estimate_fp16_model_size_bytes",
|
|
return_value = (30 * (1024**3), "config"), # 30GB fp16 model
|
|
):
|
|
required, meta = estimate_required_model_memory_gb(
|
|
"test/model",
|
|
training_type = None, # inference
|
|
load_in_4bit = True,
|
|
)
|
|
self.assertIsNotNone(required)
|
|
# 4bit base = 30/3.2 = 9.375GB, required = 9.375 + max(9.375*0.3, 2) = 12.19GB
|
|
self.assertAlmostEqual(required, 12.2, places = 0)
|
|
|
|
def test_4bit_training_reduces_base(self):
|
|
from utils.hardware.hardware import estimate_required_model_memory_gb
|
|
with patch(
|
|
"utils.hardware.hardware.estimate_fp16_model_size_bytes",
|
|
return_value = (30 * (1024**3), "config"), # 30GB fp16 model
|
|
):
|
|
required, meta = estimate_required_model_memory_gb(
|
|
"test/model",
|
|
training_type = "LoRA/QLoRA",
|
|
load_in_4bit = True,
|
|
)
|
|
self.assertIsNotNone(required)
|
|
# fallback: base=30/3.2=9.375, lora=30*0.04=1.2, act=30*0.15=4.5, cuda=1.4
|
|
self.assertAlmostEqual(required, 16.5, places = 0)
|
|
|
|
def test_full_finetune_uses_3_5x(self):
|
|
from utils.hardware.hardware import estimate_required_model_memory_gb
|
|
with patch(
|
|
"utils.hardware.hardware.estimate_fp16_model_size_bytes",
|
|
return_value = (10 * (1024**3), "config"), # 10GB model
|
|
):
|
|
required, meta = estimate_required_model_memory_gb(
|
|
"test/model",
|
|
training_type = "Full Finetuning",
|
|
)
|
|
self.assertIsNotNone(required)
|
|
# fallback: 10 * 3.5 + 1.4 cuda overhead = 36.4
|
|
self.assertAlmostEqual(required, 36.4, places = 0)
|
|
|
|
def test_returns_none_when_unavailable(self):
|
|
from utils.hardware.hardware import estimate_required_model_memory_gb
|
|
with patch(
|
|
"utils.hardware.hardware.estimate_fp16_model_size_bytes",
|
|
return_value = (None, "unavailable"),
|
|
):
|
|
required, meta = estimate_required_model_memory_gb("test/model")
|
|
self.assertIsNone(required)
|
|
|
|
|
|
class TestAutoSelectGpuIds(unittest.TestCase):
|
|
"""Automatic GPU selection by model size and free memory."""
|
|
|
|
def _make_utilization(self, devices):
|
|
"""Fake utilization response."""
|
|
return {
|
|
"available": True,
|
|
"devices": [
|
|
{
|
|
"index": idx,
|
|
"vram_total_gb": total,
|
|
"vram_used_gb": total - free,
|
|
}
|
|
for idx, total, free in devices
|
|
],
|
|
}
|
|
|
|
def test_single_gpu_sufficient(self):
|
|
from utils.hardware.hardware import auto_select_gpu_ids
|
|
import utils.hardware.hardware as hw
|
|
with (
|
|
patch.object(hw, "get_device", return_value = hw.DeviceType.CUDA),
|
|
patch.object(
|
|
hw,
|
|
"estimate_required_model_memory_gb",
|
|
return_value = (
|
|
10.0,
|
|
{
|
|
"mode": "inference",
|
|
"required_gb": 10.0,
|
|
"model_size_source": "config",
|
|
"model_size_gb": 7.7,
|
|
},
|
|
),
|
|
),
|
|
patch.object(
|
|
hw,
|
|
"_get_parent_visible_gpu_spec",
|
|
return_value = {
|
|
"raw": "0,1,2,3",
|
|
"numeric_ids": [0, 1, 2, 3],
|
|
"supports_explicit_gpu_ids": True,
|
|
},
|
|
),
|
|
patch.object(hw, "get_parent_visible_gpu_ids", return_value = [0, 1, 2, 3]),
|
|
patch.object(
|
|
hw,
|
|
"get_visible_gpu_utilization",
|
|
return_value = self._make_utilization(
|
|
[
|
|
(0, 80.0, 75.0),
|
|
(1, 80.0, 78.0),
|
|
(2, 80.0, 70.0),
|
|
(3, 80.0, 72.0),
|
|
]
|
|
),
|
|
),
|
|
):
|
|
selected, meta = auto_select_gpu_ids("test/model")
|
|
# Should pick GPU 1 (most free memory: 78GB) -- enough for 10GB
|
|
self.assertEqual(len(selected), 1)
|
|
self.assertEqual(selected[0], 1)
|
|
|
|
def test_two_gpus_needed(self):
|
|
from utils.hardware.hardware import auto_select_gpu_ids
|
|
import utils.hardware.hardware as hw
|
|
with (
|
|
patch.object(hw, "get_device", return_value = hw.DeviceType.CUDA),
|
|
patch.object(
|
|
hw,
|
|
"estimate_required_model_memory_gb",
|
|
return_value = (
|
|
50.0,
|
|
{
|
|
"mode": "inference",
|
|
"required_gb": 50.0,
|
|
"model_size_source": "config",
|
|
"model_size_gb": 38.0,
|
|
},
|
|
),
|
|
),
|
|
patch.object(
|
|
hw,
|
|
"_get_parent_visible_gpu_spec",
|
|
return_value = {
|
|
"raw": "0,1",
|
|
"numeric_ids": [0, 1],
|
|
"supports_explicit_gpu_ids": True,
|
|
},
|
|
),
|
|
patch.object(hw, "get_parent_visible_gpu_ids", return_value = [0, 1]),
|
|
patch.object(
|
|
hw,
|
|
"get_visible_gpu_utilization",
|
|
return_value = self._make_utilization(
|
|
[
|
|
(0, 40.0, 30.0), # 30GB free
|
|
(1, 40.0, 35.0), # 35GB free
|
|
]
|
|
),
|
|
),
|
|
):
|
|
selected, meta = auto_select_gpu_ids("test/model")
|
|
# 35GB (first) + 30*0.85 (second) = 60.5GB > 50GB
|
|
self.assertEqual(len(selected), 2)
|
|
|
|
def test_non_cuda_returns_none(self):
|
|
from utils.hardware.hardware import auto_select_gpu_ids
|
|
import utils.hardware.hardware as hw
|
|
with patch.object(hw, "get_device", return_value = hw.DeviceType.CPU):
|
|
selected, meta = auto_select_gpu_ids("test/model")
|
|
self.assertIsNone(selected)
|
|
self.assertEqual(meta["selection_mode"], "non_cuda")
|
|
|
|
|
|
class TestGetDeviceMap(unittest.TestCase):
|
|
"""device_map string generation."""
|
|
|
|
def test_single_gpu_returns_sequential(self):
|
|
from utils.hardware.hardware import get_device_map
|
|
import utils.hardware.hardware as hw
|
|
with (
|
|
patch.object(hw, "get_device", return_value = hw.DeviceType.CUDA),
|
|
patch.object(
|
|
hw,
|
|
"_get_parent_visible_gpu_spec",
|
|
return_value = {
|
|
"raw": "0",
|
|
"numeric_ids": [0],
|
|
"supports_explicit_gpu_ids": True,
|
|
},
|
|
),
|
|
patch.object(hw, "get_visible_gpu_count", return_value = 1),
|
|
):
|
|
dm = get_device_map(gpu_ids = [0])
|
|
self.assertEqual(dm, "sequential")
|
|
|
|
def test_multi_gpu_returns_balanced(self):
|
|
from utils.hardware.hardware import get_device_map
|
|
import utils.hardware.hardware as hw
|
|
with patch.object(hw, "get_device", return_value = hw.DeviceType.CUDA):
|
|
dm = get_device_map(gpu_ids = [0, 1])
|
|
self.assertEqual(dm, "balanced")
|
|
|
|
def test_cpu_returns_sequential(self):
|
|
from utils.hardware.hardware import get_device_map
|
|
import utils.hardware.hardware as hw
|
|
with patch.object(hw, "get_device", return_value = hw.DeviceType.CPU):
|
|
dm = get_device_map(gpu_ids = None)
|
|
self.assertEqual(dm, "sequential")
|
|
|
|
|
|
class TestResolveRequestedGpuIds(unittest.TestCase):
|
|
"""GPU ID validation."""
|
|
|
|
def test_none_returns_parent_visible(self):
|
|
from utils.hardware.hardware import resolve_requested_gpu_ids
|
|
with (
|
|
patch.dict(os.environ, {"CUDA_VISIBLE_DEVICES": "2,3"}, clear = False),
|
|
patch("utils.hardware.hardware.get_physical_gpu_count", return_value = 8),
|
|
):
|
|
result = resolve_requested_gpu_ids(None)
|
|
self.assertEqual(result, [2, 3])
|
|
|
|
def test_empty_list_returns_parent_visible(self):
|
|
from utils.hardware.hardware import resolve_requested_gpu_ids
|
|
with (
|
|
patch.dict(os.environ, {"CUDA_VISIBLE_DEVICES": "2,3"}, clear = False),
|
|
patch("utils.hardware.hardware.get_physical_gpu_count", return_value = 8),
|
|
):
|
|
result = resolve_requested_gpu_ids([])
|
|
self.assertEqual(result, [2, 3])
|
|
|
|
def test_duplicates_rejected(self):
|
|
from utils.hardware.hardware import resolve_requested_gpu_ids
|
|
with (
|
|
patch.dict(os.environ, {"CUDA_VISIBLE_DEVICES": "0,1,2"}, clear = False),
|
|
patch("utils.hardware.hardware.get_physical_gpu_count", return_value = 8),
|
|
):
|
|
with self.assertRaises(ValueError):
|
|
resolve_requested_gpu_ids([1, 1])
|
|
|
|
def test_out_of_range_rejected(self):
|
|
from utils.hardware.hardware import resolve_requested_gpu_ids
|
|
with (
|
|
patch.dict(os.environ, {"CUDA_VISIBLE_DEVICES": "0,1"}, clear = False),
|
|
patch("utils.hardware.hardware.get_physical_gpu_count", return_value = 4),
|
|
):
|
|
with self.assertRaises(ValueError):
|
|
resolve_requested_gpu_ids([5])
|
|
|
|
def test_uuid_env_var_rejects_explicit_ids(self):
|
|
from utils.hardware.hardware import resolve_requested_gpu_ids
|
|
with (
|
|
patch.dict(os.environ, {"CUDA_VISIBLE_DEVICES": "GPU-abc,GPU-def"}, clear = False),
|
|
patch("utils.hardware.hardware.get_physical_gpu_count", return_value = 8),
|
|
):
|
|
with self.assertRaises(ValueError):
|
|
resolve_requested_gpu_ids([0])
|
|
|
|
|
|
class TestApplyGpuIds(unittest.TestCase):
|
|
"""CUDA_VISIBLE_DEVICES environment variable setting."""
|
|
|
|
def test_apply_list(self):
|
|
from utils.hardware.hardware import apply_gpu_ids
|
|
with patch.dict(os.environ, {}, clear = False):
|
|
apply_gpu_ids([3, 5])
|
|
self.assertEqual(os.environ.get("CUDA_VISIBLE_DEVICES"), "3,5")
|
|
|
|
def test_apply_none_does_nothing(self):
|
|
from utils.hardware.hardware import apply_gpu_ids
|
|
|
|
original = os.environ.get("CUDA_VISIBLE_DEVICES")
|
|
apply_gpu_ids(None)
|
|
self.assertEqual(os.environ.get("CUDA_VISIBLE_DEVICES"), original)
|
|
|
|
|
|
class TestMultiGpuOverheadAccounting(unittest.TestCase):
|
|
"""Multi-GPU overhead is applied correctly.
|
|
|
|
The first GPU keeps its full free memory; the overhead factor applies
|
|
only to additional GPUs.
|
|
"""
|
|
|
|
def _make_utilization(self, devices):
|
|
return {
|
|
"available": True,
|
|
"devices": [
|
|
{
|
|
"index": idx,
|
|
"vram_total_gb": total,
|
|
"vram_used_gb": total - free,
|
|
}
|
|
for idx, total, free in devices
|
|
],
|
|
}
|
|
|
|
def test_first_gpu_not_penalized(self):
|
|
"""A model that just fits on 1 GPU should not require 2 GPUs."""
|
|
from utils.hardware.hardware import auto_select_gpu_ids
|
|
import utils.hardware.hardware as hw
|
|
|
|
# Model requires 79GB, GPU has 80GB free
|
|
with (
|
|
patch.object(hw, "get_device", return_value = hw.DeviceType.CUDA),
|
|
patch.object(
|
|
hw,
|
|
"estimate_required_model_memory_gb",
|
|
return_value = (
|
|
79.0,
|
|
{
|
|
"mode": "inference",
|
|
"required_gb": 79.0,
|
|
"model_size_source": "config",
|
|
"model_size_gb": 60.0,
|
|
},
|
|
),
|
|
),
|
|
patch.object(
|
|
hw,
|
|
"_get_parent_visible_gpu_spec",
|
|
return_value = {
|
|
"raw": "0,1",
|
|
"numeric_ids": [0, 1],
|
|
"supports_explicit_gpu_ids": True,
|
|
},
|
|
),
|
|
patch.object(hw, "get_parent_visible_gpu_ids", return_value = [0, 1]),
|
|
patch.object(
|
|
hw,
|
|
"get_visible_gpu_utilization",
|
|
return_value = self._make_utilization(
|
|
[
|
|
(0, 80.0, 80.0),
|
|
(1, 80.0, 80.0),
|
|
]
|
|
),
|
|
),
|
|
):
|
|
selected, meta = auto_select_gpu_ids("test/model")
|
|
# Should fit on 1 GPU (80GB >= 79GB)
|
|
self.assertEqual(len(selected), 1)
|
|
|
|
def test_second_gpu_has_overhead(self):
|
|
"""When 2 GPUs are needed, the second one's contribution is reduced."""
|
|
from utils.hardware.hardware import auto_select_gpu_ids
|
|
import utils.hardware.hardware as hw
|
|
|
|
# Model requires 110GB. First GPU has 80GB, second has 40GB.
|
|
# With overhead: 80 + 40*0.85 = 114GB -- just enough
|
|
with (
|
|
patch.object(hw, "get_device", return_value = hw.DeviceType.CUDA),
|
|
patch.object(
|
|
hw,
|
|
"estimate_required_model_memory_gb",
|
|
return_value = (
|
|
110.0,
|
|
{
|
|
"mode": "inference",
|
|
"required_gb": 110.0,
|
|
"model_size_source": "config",
|
|
"model_size_gb": 85.0,
|
|
},
|
|
),
|
|
),
|
|
patch.object(
|
|
hw,
|
|
"_get_parent_visible_gpu_spec",
|
|
return_value = {
|
|
"raw": "0,1",
|
|
"numeric_ids": [0, 1],
|
|
"supports_explicit_gpu_ids": True,
|
|
},
|
|
),
|
|
patch.object(hw, "get_parent_visible_gpu_ids", return_value = [0, 1]),
|
|
patch.object(
|
|
hw,
|
|
"get_visible_gpu_utilization",
|
|
return_value = self._make_utilization(
|
|
[
|
|
(0, 80.0, 80.0),
|
|
(1, 80.0, 40.0),
|
|
]
|
|
),
|
|
),
|
|
):
|
|
selected, meta = auto_select_gpu_ids("test/model")
|
|
# Should use both GPUs
|
|
self.assertEqual(len(selected), 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|