Skip to content

Commit e5a1e2f

Browse files
add gpu_health_check for computation check (flagos-ai#1064)
### PR Category Train ### PR Types Improvements ### PR Description - Fix the bug where the health check for multiple machines fails but the training task still starts. - Add unit_test for health_check - Integrate healthy node detection function: - GPU hardware validation - Computation capability verification - Usage: ``` python run.py \ --config-path ./examples/aquila/conf \ --config-name train \ train.data.data_path=/root/FlagScale/data/pile_wikipedia_demo \ +experiment.runner.enable_gpu_health_check=true \ action=run ``` - Outputs: ``` ...... ============================================================ PHASE 2: GPU HARDWARE TESTING ============================================================ Testing GPU hardware === Checking GPU 0: NVIDIA A800-SXM4-80GB === Current GPU temperature: 38°C Power usage: 79.67W / 400.00W Total memory: 81920.00 MB Used memory: 6309.62 MB GPU 0 memory utilization rate: 7.70% ... ✓ gpu_hardware: PASSED GPU hardware testing phase completed ============================================================ ============================================================ PHASE 3: GPU COMPUTATION TESTING ============================================================ Testing Float calculation... Float calculation passed Testing Double calculation... Double calculation passed Testing Half calculation... Half calculation passed Testing Endurance test (60s)... Endurance test (60s) passed Testing ECC Error Detection... ECC Error Detection: No errors detected ✓ ecc_error: PASSED ✓ computation: PASSED GPU computation testing phase completed ============================================================ ============================================================ ALL TEST PHASES COMPLETED ============================================================ ============================================================ GPU HEALTH CHECK SUMMARY ============================================================ ✓ Tensor Parallel: PASSED ✓ Data Parallel: PASSED ✓ Pipeline Parallel: PASSED ✓ Gpu Hardware: PASSED ✓ Computation: PASSED Results: 5 passed, 0 failed, 0 skipped out of 6 total 🎉 All GPU health checks PASSED! ============================================================ ``` for multiple machine: ``` [2025-10-23 15:02:04,387 FlagScale logger.py:25 INFO] Starting GPU health check before training setup... [2025-10-23 15:02:04,387 FlagScale logger.py:25 INFO] Running GPU health check across 2 nodes [2025-10-23 15:02:04,388 FlagScale logger.py:25 INFO] Checking node 0 (10.1.15.141) with 2 GPUs [2025-10-23 15:02:04,388 FlagScale logger.py:25 INFO] Running GPU health check on 10.1.15.141 (node_rank=0) [2025-10-23 15:02:04,528 FlagScale logger.py:25 INFO] Checking node 1 (10.1.15.237) with 2 GPUs [2025-10-23 15:02:04,528 FlagScale logger.py:25 INFO] Running GPU health check on 10.1.15.237 (node_rank=1) [2025-10-23 15:02:04,655 FlagScale logger.py:25 INFO] GPU health check passed on all nodes [2025-10-23 15:02:04,655 FlagScale logger.py:25 INFO] GPU health check passed successfully! [2025-10-23 15:02:04,655 FlagScale logger.py:25 INFO] Proceeding with training script generation... ``` --------- Co-authored-by: zhaoyingli <86812880+zhaoyinglia@users.noreply.github.com>
1 parent 74ef1bd commit e5a1e2f

4 files changed

Lines changed: 697 additions & 33 deletions

File tree

flagscale/runner/elastic/gpu_health_check.py

Lines changed: 313 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import argparse
2020
import os
21+
import time
2122
from datetime import timedelta
2223

2324
import torch
@@ -42,6 +43,8 @@
4243
"tensor_parallel": {"status": "pending", "error": None},
4344
"data_parallel": {"status": "pending", "error": None},
4445
"pipeline_parallel": {"status": "pending", "error": None},
46+
"gpu_hardware": {"status": "pending", "error": None},
47+
"gpu_computation": {"status": "pending", "error": None},
4548
}
4649

4750

@@ -493,6 +496,272 @@ def check_communication():
493496
print("=" * 60)
494497

495498

499+
def check_hardware_single():
500+
"""Single GPU hardware check without distributed calls"""
501+
try:
502+
import pynvml
503+
504+
pynvml.nvmlInit()
505+
506+
print("Checking GPU hardware")
507+
508+
device_count = pynvml.nvmlDeviceGetCount()
509+
all_passed = True
510+
errors = []
511+
for i in range(device_count):
512+
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
513+
gpu_name = pynvml.nvmlDeviceGetName(handle)
514+
515+
print(f"=== Checking GPU {i}: {gpu_name} ===")
516+
517+
temp = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
518+
print(f"Current GPU temperature: {temp}°C")
519+
if temp >= 90:
520+
all_passed = False
521+
errors.append(f"GPU {i} overheat: {temp}°C")
522+
elif temp > 85:
523+
print(f"Warning: GPU {i} high temperature: {temp}°C")
524+
525+
power_usage = pynvml.nvmlDeviceGetPowerUsage(handle) / 1000.0
526+
power_limit = pynvml.nvmlDeviceGetEnforcedPowerLimit(handle) / 1000.0
527+
power_ratio = power_usage / power_limit if power_limit > 0 else 0.0
528+
print(f"Power usage: {power_usage:.2f}W / {power_limit:.2f}W")
529+
if power_ratio > 0.9:
530+
print(f"GPU {i} power usage high: {power_usage:.1f}W ({power_ratio:.0%})")
531+
532+
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
533+
memory_utilization = (float(mem_info.used) / float(mem_info.total)) * 100
534+
535+
print(f"Total memory: {float(mem_info.total) / (1024**2):.2f} MB")
536+
print(f"Used memory: {float(mem_info.used) / (1024**2):.2f} MB")
537+
print(f"GPU {i} memory utilization rate: {memory_utilization:.2f}%")
538+
539+
if memory_utilization >= 98.0:
540+
all_passed = False
541+
errors.append(f"GPU {i} memory almost full: {memory_utilization * 100:.1f}%")
542+
543+
pynvml.nvmlShutdown()
544+
if all_passed:
545+
log_check_result("gpu_hardware", status="passed")
546+
return True
547+
else:
548+
log_check_result("gpu_hardware", status="failed", error="; ".join(errors))
549+
return False
550+
except ImportError:
551+
log_check_result("gpu_hardware", status="failed", error="pynvml is not installed")
552+
return False
553+
except Exception as e:
554+
log_check_result("gpu_hardware", status="failed", error=str(e))
555+
return False
556+
557+
558+
def check_hardware():
559+
"""Distributed wrapper: run single check once per node and summarize result"""
560+
561+
args = _GLOBAL_ARGS
562+
rank = dist.get_rank()
563+
564+
if rank == 0:
565+
print("\n" + "=" * 60)
566+
print("PHASE 2: GPU HARDWARE TESTING")
567+
print("=" * 60)
568+
569+
if args.local_rank == 0:
570+
check_hardware_single()
571+
if rank == 0:
572+
print("\nGPU hardware checking phase completed")
573+
print("=" * 60)
574+
575+
576+
def check_computation_for_different_dtype(dtype, name):
577+
args = _GLOBAL_ARGS
578+
check_tensor = torch.randn(4096, 4096, dtype=dtype).to(f"cuda:{args.local_rank}")
579+
result = torch.matmul(check_tensor, check_tensor)
580+
if torch.any(torch.isnan(result)):
581+
print(f"{name} failed: nan is detected in result")
582+
return False
583+
elif torch.any(torch.isinf(result)):
584+
print(f"{name} failed: inf is detected in result")
585+
return False
586+
else:
587+
return True
588+
589+
590+
def check_computation_endurance():
591+
args = _GLOBAL_ARGS
592+
start_time = time.time()
593+
iteration = 0
594+
595+
while time.time() - start_time < 60:
596+
iteration += 1
597+
598+
a = torch.randn(4096, 4096).to(f"cuda:{args.local_rank}")
599+
b = torch.randn(4096, 4096).to(f"cuda:{args.local_rank}")
600+
result1 = torch.matmul(a, b)
601+
602+
c = torch.randn(4096, 4096).to(f"cuda:{args.local_rank}")
603+
result2 = torch.inverse(c)
604+
605+
if torch.any(torch.isnan(result1)) or torch.any(torch.isnan(result2)):
606+
print(f"check_computation_endurance failed: nan detected in iteration {iteration}")
607+
return False
608+
609+
return True
610+
611+
612+
def check_ecc_error():
613+
"""Check ECC Error through matrix multiplication operations"""
614+
try:
615+
args = _GLOBAL_ARGS
616+
rank = dist.get_rank()
617+
device = f"cuda:{args.local_rank}"
618+
619+
# Perform multiple matrix operations to stress check memory
620+
for i in range(5):
621+
# Create large tensors to stress GPU memory
622+
tensor_a = torch.randn(2048, 2048, dtype=torch.float32, device=device)
623+
tensor_b = torch.randn(2048, 2048, dtype=torch.float32, device=device)
624+
625+
# Perform matrix multiplication that could trigger ECC errors
626+
result = torch.matmul(tensor_a, tensor_b)
627+
628+
# Check for abnormal values that might indicate ECC errors
629+
if torch.any(torch.isnan(result)):
630+
print(f"ECC Error Detection: NaN detected in iteration {i}")
631+
return False
632+
if torch.any(torch.isinf(result)):
633+
print(f"ECC Error Detection: Inf detected in iteration {i}")
634+
return False
635+
636+
torch.cuda.empty_cache()
637+
if rank == 0:
638+
print("ECC Error Detection: No errors detected")
639+
return True
640+
641+
except torch.cuda.OutOfMemoryError as e:
642+
print(f"ECC Error Detection failed: GPU out of memory - {e}")
643+
return False
644+
except RuntimeError as e:
645+
if "cuda" in str(e).lower() or "gpu" in str(e).lower():
646+
print(f"ECC Error Detection failed: GPU runtime error - {e}")
647+
return False
648+
else:
649+
print(f"ECC Error Detection failed: Runtime error - {e}")
650+
return False
651+
except Exception as e:
652+
print(f"ECC Error Detection failed: Unexpected error - {e}")
653+
return False
654+
655+
656+
def check_computation_single():
657+
"""Single GPU computation check"""
658+
print("Checking GPU computation capabilities...")
659+
checks = [
660+
("Float", torch.float32),
661+
("Double", torch.double),
662+
("Half", torch.half),
663+
]
664+
all_pass = True
665+
for display_name, dtype in checks:
666+
ok = check_computation_for_different_dtype(
667+
dtype=dtype, name=f"check_calculation_{display_name.lower()}"
668+
)
669+
print(f"{display_name} computation: {'PASS' if ok else 'FAIL'}")
670+
if not ok:
671+
all_pass = False
672+
print("Starting 60-second endurance check...")
673+
endurance_result = check_computation_endurance()
674+
if not endurance_result:
675+
all_pass = False
676+
print(f"Endurance check: {'PASS' if endurance_result else 'FAIL'}")
677+
ecc_error_result = check_ecc_error()
678+
if not ecc_error_result:
679+
all_pass = False
680+
print(f"ECC error check: {'PASS' if ecc_error_result else 'FAIL'}")
681+
if all_pass:
682+
log_check_result("gpu_computation", "passed")
683+
return True
684+
else:
685+
log_check_result("gpu_computation", "failed")
686+
return False
687+
688+
689+
def check_computation():
690+
"""Test GPU computation capabilities with distributed coordination"""
691+
args = _GLOBAL_ARGS
692+
rank = dist.get_rank()
693+
694+
if rank == 0:
695+
print("\n" + "=" * 60)
696+
print("PHASE 3: GPU COMPUTATION CHECKING")
697+
print("=" * 60)
698+
699+
# Check individual computation capabilities
700+
check_functions = [
701+
("Float computation", torch.float32, "check_computation_float"),
702+
("Double computation", torch.double, "check_computation_double"),
703+
("Half computation", torch.half, "check_computation_half"),
704+
("Endurance check (60s)", check_computation_endurance),
705+
("ECC Error Detection", check_ecc_error),
706+
]
707+
708+
all_passed = True
709+
failed_checks = []
710+
711+
for check_name, *rest in check_functions:
712+
if rank == 0:
713+
print(f"\nTesting {check_name}...")
714+
715+
try:
716+
if isinstance(rest[0], torch.dtype):
717+
_, dtype, report_name = check_name, rest[0], rest[1]
718+
result = check_computation_for_different_dtype(dtype, report_name)
719+
else:
720+
check_func = rest[0]
721+
result = check_func()
722+
if not result:
723+
all_passed = False
724+
failed_checks.append(check_name)
725+
726+
except Exception as e:
727+
result = False
728+
all_passed = False
729+
failed_checks.append(check_name)
730+
if rank == 0:
731+
print(f"✗ {check_name} failed with exception: {e}")
732+
733+
try:
734+
result_tensor = torch.zeros(args.world_size).to(f"cuda:{args.local_rank}")
735+
result_tensor[args.rank] = 1.0 if result else 0.0
736+
dist.all_reduce(result_tensor, dist.ReduceOp.SUM)
737+
738+
if args.rank == 0:
739+
expected_tensor = torch.ones_like(result_tensor).to(f"cuda:{args.local_rank}")
740+
if torch.allclose(result_tensor, expected_tensor, atol=1e-6):
741+
print(f"{check_name} passed")
742+
else:
743+
print(f"{check_name} failed")
744+
except Exception as e:
745+
if rank == 0:
746+
print(f"⚠ Warning: Failed to gather {check_name} results: {e}")
747+
748+
try:
749+
dist.barrier()
750+
except Exception as e:
751+
if rank == 0:
752+
print(f"⚠ Warning: Calculation check barrier failed: {e}")
753+
754+
if all_passed:
755+
log_check_result("gpu_computation", "passed")
756+
else:
757+
error_msg = f"Failed checks: {', '.join(failed_checks)}"
758+
log_check_result("gpu_computation", "failed", error_msg)
759+
760+
if rank == 0:
761+
print("\nGPU computation checking phase completed")
762+
print("=" * 60)
763+
764+
496765
def parse_args():
497766
parser = argparse.ArgumentParser(description="GPU Health Check arguments")
498767
parser.add_argument(
@@ -535,18 +804,28 @@ def print_check_summary():
535804
rank = dist.get_rank() if dist.is_initialized() else 0
536805
if rank != 0:
537806
return
538-
807+
args = _GLOBAL_ARGS
808+
world_size = args.world_size
539809
print("=" * 60)
540810
print("GPU HEALTH CHECK SUMMARY")
541811
print("=" * 60)
812+
if world_size == 1:
813+
results = list(_CHECK_RESULTS.items())[-2:]
814+
scope_desc = "HARDWARE ONLY (single GPU)"
815+
else:
816+
results = list(_CHECK_RESULTS.items())
817+
scope_desc = "ALL CHECKS (multi GPU)"
818+
819+
print(f"Scope: {scope_desc}")
820+
print("-" * 60)
542821

543-
total_checks = len(_CHECK_RESULTS)
544-
passed_checks = sum(1 for result in _CHECK_RESULTS.values() if result["status"] == "passed")
545-
failed_checks = sum(1 for result in _CHECK_RESULTS.values() if result["status"] == "failed")
546-
skipped_checks = sum(1 for result in _CHECK_RESULTS.values() if result["status"] == "skipped")
547-
pending_checks = sum(1 for result in _CHECK_RESULTS.values() if result["status"] == "pending")
822+
total_checks = len(results)
823+
passed_checks = sum(1 for _, r in results if r["status"] == "passed")
824+
failed_checks = sum(1 for _, r in results if r["status"] == "failed")
825+
skipped_checks = sum(1 for _, r in results if r["status"] == "skipped")
826+
pending_checks = sum(1 for _, r in results if r["status"] == "pending")
548827

549-
for check_name, result in _CHECK_RESULTS.items():
828+
for check_name, result in results:
550829
status_icon = (
551830
"✓" if result["status"] == "passed" else "✗" if result["status"] == "failed" else "⚠"
552831
)
@@ -595,7 +874,24 @@ def main():
595874
if rank == 0:
596875
print("Single process mode detected")
597876
print("Running basic GPU hardware and computation checks...")
598-
# TODO: add GPU hardware and computation checks
877+
878+
# PHASE 1: Check gpu hardware
879+
safe_check_execution(check_hardware_single, "gpu_hardware", timeout_seconds=60)
880+
# PHASE 2: Check gpu computation capabilities
881+
safe_check_execution(
882+
check_computation_single, "gpu_computation", timeout_seconds=300
883+
) # 5 minutes for endurance check
884+
# Print final summary
885+
if rank == 0:
886+
print_check_summary()
887+
SINGLE_CHECK_RESULTS = ["gpu_hardware", "gpu_computation"]
888+
failed_count = sum(
889+
1 for r in SINGLE_CHECK_RESULTS if _CHECK_RESULTS[r]["status"] == "failed"
890+
)
891+
if failed_count > 0:
892+
import sys
893+
894+
sys.exit(1)
599895
return
600896

601897
if rank == 0:
@@ -612,7 +908,11 @@ def main():
612908
# PHASE 1: Check parallel communication
613909
check_communication()
614910

615-
# TODO: add GPU hardware and computation checks
911+
# PHASE 2: Check gpu hardware
912+
check_hardware()
913+
914+
# PHASE 3: Check gpu computation capabilities
915+
check_computation()
616916

617917
if rank == 0:
618918
print("=" * 60)
@@ -636,11 +936,11 @@ def main():
636936
if rank == 0:
637937
print_check_summary()
638938

639-
failed_count = sum(1 for r in _CHECK_RESULTS.values() if r["status"] == "failed")
640-
if failed_count > 0:
641-
import sys
939+
failed_count = sum(1 for r in _CHECK_RESULTS.values() if r["status"] == "failed")
940+
if failed_count > 0:
941+
import sys
642942

643-
sys.exit(1)
943+
sys.exit(1)
644944

645945

646946
if __name__ == "__main__":

0 commit comments

Comments
 (0)