test_vram_capacity.py 4.6 KB

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