test_vram_capacity.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """Tests for additive GPU VRAM capacity + telemetry + capacity-aware placement.
  2. Covers: nvidia-smi parser (capacity/free/telemetry, N/A + failure robustness),
  3. heartbeat round-trip, registry persistence + aggregation, capacity-aware placement
  4. tie-break, and the declared-requirement (required_vram_mb) plumbing for anti-OOM.
  5. Runs under pytest OR standalone: `python tests/test_vram_capacity.py`.
  6. """
  7. import sys
  8. from unittest import mock
  9. sys.path.insert(0, "/srv") # make `import fabric.*` resolve when run directly
  10. from fabric.shared.schemas import (
  11. Heartbeat, GpuMemInfo, ModuleStatus, ModuleState, PlaceRequest,
  12. ModuleManifest, GpuResources,
  13. )
  14. from fabric.control.registry import Registry
  15. from fabric.agent.systemd_wrap import SystemdWrapRuntime
  16. import fabric.agent.main as agent
  17. FAKE_SMI = (
  18. "0, NVIDIA L40S, 49140, 29894, 45, 62, 210.50, 550.54.15\n"
  19. "1, NVIDIA L40S, 49140, 1024, 0, 38, 90.00, 550.54.15\n"
  20. )
  21. def test_get_gpu_info_parses_capacity_and_telemetry():
  22. with mock.patch.object(agent.subprocess, "run",
  23. lambda *a, **k: mock.Mock(returncode=0, stdout=FAKE_SMI)):
  24. info = agent._get_gpu_info()
  25. assert len(info["gpus"]) == 2
  26. g0 = info["gpus"][0]
  27. assert g0["gpu_index"] == 0
  28. assert g0["name"] == "NVIDIA L40S"
  29. assert g0["mem_total_mb"] == 49140
  30. assert g0["mem_used_mb"] == 29894
  31. assert g0["mem_free_mb"] == 49140 - 29894
  32. assert g0["util_pct"] == 45
  33. assert g0["temp_c"] == 62
  34. assert g0["power_w"] == 210.50
  35. assert info["driver_version"] == "550.54.15"
  36. assert info["vram_total_mb"] == 49140 * 2
  37. assert info["vram_free_mb"] == (49140 - 29894) + (49140 - 1024)
  38. def test_get_gpu_info_handles_na_and_failure():
  39. na = "0, GPU, 24576, 100, [N/A], [N/A], [N/A], 550.0\n"
  40. with mock.patch.object(agent.subprocess, "run",
  41. lambda *a, **k: mock.Mock(returncode=0, stdout=na)):
  42. info = agent._get_gpu_info()
  43. g = info["gpus"][0]
  44. assert g["mem_free_mb"] == 24576 - 100
  45. assert g["util_pct"] is None and g["temp_c"] is None and g["power_w"] is None
  46. with mock.patch.object(agent.subprocess, "run",
  47. lambda *a, **k: mock.Mock(returncode=9, stdout="")):
  48. empty = agent._get_gpu_info()
  49. assert empty["gpus"] == [] and empty["vram_total_mb"] == 0
  50. with mock.patch.object(agent.subprocess, "run",
  51. mock.Mock(side_effect=FileNotFoundError())):
  52. empty2 = agent._get_gpu_info()
  53. assert empty2["vram_total_mb"] == 0 and empty2["vram_free_mb"] == 0
  54. def test_heartbeat_roundtrip_carries_capacity():
  55. hb = Heartbeat(
  56. node_id="n1", node_ip="1.2.3.4",
  57. gpus=[{"gpu_index": 0, "mem_total_mb": 49140, "mem_used_mb": 140, "mem_free_mb": 49000}],
  58. vram_total_mb=49140, vram_free_mb=49000, driver_version="550.0",
  59. )
  60. d = hb.model_dump()
  61. assert d["vram_total_mb"] == 49140 and d["vram_free_mb"] == 49000
  62. assert d["gpus"][0]["mem_free_mb"] == 49000
  63. hb2 = Heartbeat(**d)
  64. assert hb2.vram_total_mb == 49140
  65. assert isinstance(hb2.gpus[0], GpuMemInfo)
  66. def test_registry_carries_and_aggregates_capacity():
  67. reg = Registry()
  68. reg.handle_heartbeat(Heartbeat(node_id="a", node_ip="10.0.0.1",
  69. vram_total_mb=49140, vram_free_mb=20000))
  70. reg.handle_heartbeat(Heartbeat(node_id="b", node_ip="10.0.0.2",
  71. vram_total_mb=24576, vram_free_mb=10000))
  72. nodes = {n.node_id: n for n in reg.get_alive_nodes()}
  73. assert nodes["a"].vram_total_mb == 49140 and nodes["a"].vram_free_mb == 20000
  74. assert reg.total_gpu_capacity_mb() == 49140 + 24576
  75. assert reg.total_gpu_free_mb() == 20000 + 10000
  76. def test_place_prefers_node_with_more_free_vram():
  77. reg = Registry()
  78. mod = ModuleStatus(name="m", state=ModuleState.loaded, infer_url="http://x")
  79. reg.handle_heartbeat(Heartbeat(node_id="low", node_ip="10.0.0.1",
  80. modules=[mod], vram_total_mb=49140, vram_free_mb=5000))
  81. reg.handle_heartbeat(Heartbeat(node_id="high", node_ip="10.0.0.2",
  82. modules=[mod], vram_total_mb=49140, vram_free_mb=40000))
  83. res = reg.place(PlaceRequest(model="m"))
  84. assert res is not None and res.node_id == "high"
  85. def test_systemd_wrap_reports_required_vram_from_manifest():
  86. man = ModuleManifest(name="big", resources=GpuResources(gpus=1, vram_mb=20480))
  87. assert SystemdWrapRuntime(man).get_status().required_vram_mb == 20480
  88. # default resources -> vram_mb="auto" -> reported as 0 (unknown)
  89. assert SystemdWrapRuntime(ModuleManifest(name="auto")).get_status().required_vram_mb == 0
  90. def test_registry_module_carries_required_vram():
  91. reg = Registry()
  92. mod = ModuleStatus(name="m", state=ModuleState.unloaded, required_vram_mb=20480)
  93. reg.handle_heartbeat(Heartbeat(node_id="n", node_ip="1.1.1.1",
  94. modules=[mod], vram_total_mb=49140, vram_free_mb=5000))
  95. cands = reg.find_module("m")
  96. assert cands and cands[0][1].required_vram_mb == 20480
  97. if __name__ == "__main__":
  98. fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
  99. failed = 0
  100. for fn in fns:
  101. try:
  102. fn()
  103. print("PASS", fn.__name__)
  104. except Exception as e:
  105. failed += 1
  106. print("FAIL", fn.__name__, "->", repr(e))
  107. print("\n%d/%d passed" % (len(fns) - failed, len(fns)))
  108. sys.exit(1 if failed else 0)