"""観測結果の解析と使い方の型のテスト(SPEC_03「取得」「使い方の型」「表示」)."""
from __future__ import annotations
import pytest
from probe_helpers import PMON_HEADER
from tias_judge_model.remote.observe import (
GpuSample,
UsagePattern,
classify_usage_pattern,
format_elapsed,
parse_gpu_samples,
parse_pmon,
summarize_samples,
)
def _samples(utils: list[int], memory: int = 10) -> list[GpuSample]:
return [GpuSample(util, memory) for util in utils]
def _pattern(utils: list[int], process_count: int | None) -> UsagePattern | None:
usage = summarize_samples(_samples(utils))
assert usage is not None
return classify_usage_pattern(usage, process_count)
class TestParseGpuSamples:
def test_groups_samples_by_gpu_index_in_order(self) -> None:
text = "0, 5, 300\n1, 0, 10\n0, 74, 310\n1, 0, 12\n"
samples = parse_gpu_samples(text)
assert samples[0] == [GpuSample(5, 300), GpuSample(74, 310)]
assert samples[1] == [GpuSample(0, 10), GpuSample(0, 12)]
def test_empty_output_has_no_samples(self) -> None:
assert parse_gpu_samples("") == {}
class TestSummarizeSamples:
def test_mean_and_max(self) -> None:
usage = summarize_samples(
[GpuSample(0, 100), GpuSample(74, 300), GpuSample(10, 200), GpuSample(0, 50)]
)
assert usage is not None
assert usage.sample_count == 4
assert usage.active_count == 2
assert usage.mean_utilization_percent == pytest.approx(21.0)
assert usage.max_utilization_percent == 74
assert usage.max_memory_used_mib == 300
def test_no_samples(self) -> None:
assert summarize_samples([]) is None
class TestUsagePattern:
@pytest.mark.parametrize(
("utils", "expected"),
[
([1] * 10, UsagePattern.CONSTANT),
([1] * 8 + [0] * 2, UsagePattern.CONSTANT), # ちょうど 80%
([1] * 4 + [0], UsagePattern.CONSTANT), # 4/5 = 80%
([1] * 7 + [0] * 3, UsagePattern.INTERMITTENT), # 70%
([1] * 79 + [0] * 21, UsagePattern.INTERMITTENT), # 79%
([1] + [0] * 9, UsagePattern.INTERMITTENT), # 1 件
],
)
def test_active_ratio_boundaries(self, utils: list[int], expected: UsagePattern) -> None:
# 使用率が 0% を超えたサンプルがあれば,計算プロセスの有無によらず常時/断続的
assert _pattern(utils, 0) is expected
assert _pattern(utils, 2) is expected
def test_memory_only(self) -> None:
assert _pattern([0, 0, 0], 1) is UsagePattern.MEMORY_ONLY
def test_idle(self) -> None:
assert _pattern([0, 0, 0], 0) is UsagePattern.IDLE
class TestParsePmon:
def test_max_sm_per_compute_process(self) -> None:
text = "\n".join(
[
*PMON_HEADER,
" 0 1234 C 10 1 - - - - python",
" 1 5678 C 0 0 - - - - python",
" 0 1234 C 74 9 - - - - python",
" 0 1234 C 3 9 - - - - python",
]
)
assert parse_pmon(text) == {1234: 74, 5678: 0}
def test_header_dash_and_graphics_rows_are_excluded(self) -> None:
text = "\n".join(
[
*PMON_HEADER,
" 0 - - - - - - - - -",
" 0 999 G 50 2 - - - - Xorg",
" 0 1234 C - - - - - - python",
" 0 1234 C 20 - - - - - python",
]
)
peaks = parse_pmon(text)
assert peaks == {1234: 20}
def test_without_header_is_unavailable(self) -> None:
assert parse_pmon("") is None
class TestFormatElapsed:
def test_spec_example_hours(self) -> None:
# SPEC_03「表示」の例: 27.7 時間
assert format_elapsed(99720) == "27.7 時間"