Skip to content

Commit 03c4ebf

Browse files
add unin_tests and fix some bugs
1 parent ae59357 commit 03c4ebf

5 files changed

Lines changed: 439 additions & 28 deletions

File tree

flagscale/runner/runner_train.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -798,35 +798,35 @@ def query(self, interval=10, timeout=None):
798798
cur_time = time.time()
799799
logger.info(f"Query timeout reached ({timeout}s)")
800800

801-
<<<<<<< HEAD
802-
=======
803801
def _monitor_and_collect_logs(self):
804802
"""Monitor training status and collect logs in the background"""
805803
self._monitoring_active = True
806804
try:
807805
while self._monitoring_active:
808806
status = self._query_status()
809-
807+
810808
if hasattr(self, 'resources') and self.resources:
811809
for node_rank, (host, _) in enumerate(self.resources.items()):
812810
if not self._monitoring_active:
813811
break
814-
self._collect_and_diagnose_single_node(host, node_rank,
815-
status != JobStatus.RUNNING)
812+
self._collect_and_diagnose_single_node(
813+
host, node_rank, status != JobStatus.RUNNING
814+
)
816815
else:
817816
# Single machine
818-
self._collect_and_diagnose_single_node("localhost", 0,
819-
status != JobStatus.RUNNING)
820-
817+
self._collect_and_diagnose_single_node(
818+
"localhost", 0, status != JobStatus.RUNNING
819+
)
820+
821821
if status == JobStatus.COMPLETED_OR_IDLE:
822822
logger.info("Training completed. Final log collection finished.")
823823
break
824-
824+
825825
for _ in range(10):
826826
if not self._monitoring_active:
827827
break
828828
time.sleep(1)
829-
829+
830830
except Exception as e:
831831
logger.error(f"Error in log monitoring: {e}")
832832
finally:
@@ -836,49 +836,56 @@ def _collect_and_diagnose_single_node(self, host, node_rank, process_completed=F
836836
"""Collect and diagnose logs for a single node"""
837837
try:
838838
logging_config = self.config.train.system.logging
839-
839+
840840
log_file = collect_logs(
841-
self.config, host, node_rank,
842-
logging_config.diagnostic_dir,
841+
self.config,
842+
host,
843+
node_rank,
844+
logging_config.diagnostic_dir,
843845
dryrun=False,
844-
process_running=not process_completed
846+
process_running=not process_completed,
845847
)
846-
848+
847849
if log_file and os.path.exists(log_file) and os.path.getsize(log_file) > 0:
848850
diagnostic_report = generate_diagnostic_report(
849851
self.config, host, node_rank, log_file, return_content=True
850852
)
851-
853+
852854
combined_file = os.path.join(
853-
logging_config.diagnostic_dir,
854-
f"host_{node_rank}_{host}_combined.log"
855+
logging_config.diagnostic_dir, f"host_{node_rank}_{host}_combined.log"
855856
)
856-
857+
857858
os.makedirs(os.path.dirname(combined_file), exist_ok=True)
858-
859+
859860
with open(log_file, 'r') as log_f:
860861
log_content = log_f.read()
861-
862+
862863
with open(combined_file, 'a') as f:
863-
f.write(f"\n=== Collection Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===\n")
864+
f.write(
865+
f"\n=== Collection Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===\n"
866+
)
864867
f.write("=== Log Content ===\n")
865868
f.write(log_content)
866869
f.write("\n=== Diagnostic Report ===\n")
867-
f.write(diagnostic_report if diagnostic_report else "No diagnostic report generated\n")
868-
870+
f.write(
871+
diagnostic_report
872+
if diagnostic_report
873+
else "No diagnostic report generated\n"
874+
)
875+
869876
logger.debug(f"Updated combined log for {host} (node {node_rank})")
870-
877+
871878
try:
872879
os.remove(log_file)
873880
except OSError as e:
874881
logger.warning(f"Could not remove temporary log file {log_file}: {e}")
875-
882+
876883
if process_completed:
877884
reset_log_collection(host, node_rank)
878-
885+
879886
except Exception as e:
880887
logger.error(f"Failed to collect and diagnose logs for {host} (node {node_rank}): {e}")
881-
>>>>>>> ba556ac53b36e1272f8c0ec5b6ee476df9293d64
888+
882889

883890
class CloudTrainRunner(RunnerBase):
884891
def __init__(self, config: DictConfig):

tests/scripts/unit_tests/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,12 @@ flagscale:
135135
- export NVTE_FLASH_ATTN=0
136136
- export NVTE_FUSED_ATTN=0
137137
- ulimit -n 65535
138+
coverage_fold:
139+
flagscale
138140
subset:
141+
elastic:
142+
type: batch
143+
depth: all
139144
runner:
140145
type: batch
141146
depth: all

tests/unit_tests/runner/elastic/__init__.py

Whitespace-only changes.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import os
2+
import tempfile
3+
4+
from unittest.mock import MagicMock, mock_open, patch
5+
6+
import pytest
7+
8+
from flagscale.runner.elastic.diagnostic import error_types, generate_diagnostic_report
9+
10+
11+
class TestDiagnostic:
12+
"""Test cases for diagnostic module"""
13+
14+
@pytest.fixture
15+
def mock_config(self):
16+
"""Mock config object"""
17+
return MagicMock()
18+
19+
@pytest.fixture
20+
def sample_log_content(self):
21+
"""Sample log content with various errors"""
22+
return """
23+
[INFO] Starting training...
24+
[ERROR] CUDA out of memory
25+
Traceback (most recent call last):
26+
File "train.py", line 100, in <module>
27+
model.forward()
28+
torch.distributed.elastic.rendezvous.api.RendezvousConnectionError: Connection failed
29+
OutOfMemoryError: GPU memory exhausted
30+
[ERROR] Training failed
31+
"""
32+
33+
def test_error_types_dict_exists(self):
34+
"""Test that error_types dictionary is properly defined"""
35+
assert isinstance(error_types, dict)
36+
assert len(error_types) > 0
37+
38+
expected_keys = ['out of memory', 'rendezvous', 'traceback', 'error', 'cuda']
39+
for key in expected_keys:
40+
assert key in error_types
41+
42+
def test_generate_diagnostic_report_empty_file(self, mock_config):
43+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
44+
f.write("")
45+
temp_path = f.name
46+
47+
try:
48+
report = generate_diagnostic_report(
49+
mock_config, "localhost", 0, temp_path, return_content=True
50+
)
51+
52+
assert "Diagnostic Report for localhost (node 0)" in report
53+
assert "Log file is empty" in report
54+
finally:
55+
os.unlink(temp_path)
56+
57+
def test_generate_diagnostic_report_with_errors(self, mock_config, sample_log_content):
58+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
59+
f.write(sample_log_content)
60+
temp_path = f.name
61+
62+
try:
63+
report = generate_diagnostic_report(
64+
mock_config, "localhost", 0, temp_path, return_content=True
65+
)
66+
67+
assert "Diagnostic Report for localhost (node 0)" in report
68+
assert "OutOfMemoryError" in report
69+
assert "RendezvousConnectionError" in report
70+
assert "CodeError" in report
71+
assert "GeneralError" in report
72+
finally:
73+
os.unlink(temp_path)
74+
75+
def test_generate_diagnostic_report_nonexistent_file(self, mock_config):
76+
report = generate_diagnostic_report(
77+
mock_config, "localhost", 0, "/nonexistent/file.log", return_content=True
78+
)
79+
80+
assert "Diagnostic Report for localhost (node 0)" in report
81+
assert "Log file is empty or does not exist" in report
82+
83+
def test_generate_diagnostic_report_no_errors(self, mock_config):
84+
content = """
85+
[INFO] Training started successfully
86+
[INFO] Epoch 1/100 completed
87+
[INFO] Training finished successfully
88+
"""
89+
90+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
91+
f.write(content)
92+
temp_path = f.name
93+
94+
try:
95+
report = generate_diagnostic_report(
96+
mock_config, "localhost", 0, temp_path, return_content=True
97+
)
98+
99+
assert "Diagnostic Report for localhost (node 0)" in report
100+
assert "No errors or unknown error detected" in report
101+
finally:
102+
os.unlink(temp_path)
103+
104+
def test_generate_diagnostic_report_file_output(self, mock_config, sample_log_content):
105+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
106+
f.write(sample_log_content)
107+
temp_path = f.name
108+
109+
try:
110+
# Test file output mode
111+
result_path = generate_diagnostic_report(
112+
mock_config, "localhost", 0, temp_path, return_content=False
113+
)
114+
115+
assert result_path is not None
116+
assert "diagnostic" in result_path or result_path.endswith('.txt')
117+
finally:
118+
os.unlink(temp_path)
119+
120+
@patch('flagscale.runner.elastic.diagnostic.logger')
121+
def test_generate_diagnostic_report_read_error(self, mock_logger, mock_config):
122+
"""Test diagnostic report generation with file read error"""
123+
with patch('builtins.open', side_effect=PermissionError("Access denied")):
124+
report = generate_diagnostic_report(
125+
mock_config, "localhost", 0, "/some/file.log", return_content=True
126+
)
127+
128+
assert "Error reading log file" in report
129+
mock_logger.error.assert_called()
130+
131+
def test_error_detection_case_insensitive(self, mock_config):
132+
content = """
133+
CUDA OUT OF MEMORY ERROR occurred
134+
TRACEBACK: Failed to execute
135+
RENDEZVOUS connection failed
136+
"""
137+
138+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
139+
f.write(content)
140+
temp_path = f.name
141+
142+
try:
143+
report = generate_diagnostic_report(
144+
mock_config, "localhost", 0, temp_path, return_content=True
145+
)
146+
147+
# Should detect errors regardless of case
148+
assert "OutOfMemoryError" in report
149+
assert "CodeError" in report
150+
assert "RendezvousError" in report
151+
finally:
152+
os.unlink(temp_path)
153+
154+
def test_multiple_error_types_detection(self, mock_config):
155+
content = """
156+
CUDA error: out of memory
157+
ImportError: module not found
158+
Permission denied: cannot access file
159+
Connection timeout occurred
160+
Process was killed
161+
"""
162+
163+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
164+
f.write(content)
165+
temp_path = f.name
166+
167+
try:
168+
report = generate_diagnostic_report(
169+
mock_config, "localhost", 0, temp_path, return_content=True
170+
)
171+
172+
assert "CUDAError" in report
173+
assert "ImportError" in report
174+
assert "PermissionError" in report
175+
assert "TimeoutError" in report
176+
assert "ProcessKilled" in report
177+
finally:
178+
os.unlink(temp_path)

0 commit comments

Comments
 (0)