Skip to content

Commit 436bdfe

Browse files
committed
feat: replace eval Rich spinner with plain print observability (matching training format)
1 parent 1572b32 commit 436bdfe

2 files changed

Lines changed: 205 additions & 81 deletions

File tree

‎sagemaker-train/src/sagemaker/train/evaluate/execution.py‎

Lines changed: 98 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,87 +1161,104 @@ def get_cached_mlflow_url():
11611161

11621162
time.sleep(poll)
11631163
else:
1164-
# Terminal experience with rich library
1165-
try:
1166-
from rich.live import Live
1167-
from rich.panel import Panel
1168-
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
1169-
from rich.console import Group
1170-
from rich.status import Status
1171-
from rich.style import Style
1172-
1173-
progress = Progress(
1174-
SpinnerColumn("bouncingBar"),
1175-
TextColumn("{task.description}"),
1176-
TimeElapsedColumn(),
1177-
)
1178-
progress.add_task(f"Waiting for PipelineExecution to reach [bold]{target_status}[/bold] status...")
1179-
status = Status("Current status:")
1180-
1181-
with Live(
1182-
Panel(
1183-
Group(progress, status),
1184-
title="Wait Log Panel",
1185-
border_style=Style(color="blue"),
1186-
),
1187-
transient=True,
1188-
):
1189-
while True:
1190-
self.refresh()
1191-
current_status = self.status.overall_status
1192-
status.update(f"Current status: [bold]{current_status}[/bold]")
1193-
1194-
if target_status == current_status:
1195-
logger.info(f"Final Resource Status: [bold]{current_status}[/bold]")
1196-
return
1197-
1198-
if "failed" in current_status.lower():
1199-
from sagemaker.core.utils.exceptions import FailedStatusError
1200-
raise FailedStatusError(
1201-
resource_type="PipelineExecution",
1202-
status=current_status,
1203-
reason=self.status.failure_reason,
1204-
)
1205-
1206-
if timeout is not None and time.time() - start_time >= timeout:
1207-
from sagemaker.core.utils.exceptions import TimeoutExceededError
1208-
raise TimeoutExceededError(
1209-
resource_type="EvaluationJob",
1210-
status=current_status,
1211-
message="Your evaluation job is still running. Call .refresh() to check its current status.",
1212-
)
1213-
1214-
time.sleep(poll)
1215-
except ImportError:
1216-
# Fallback to simple print-based progress if rich is not available
1217-
logger.info(f"Waiting for PipelineExecution to reach {target_status} status...")
1218-
while True:
1219-
self.refresh()
1220-
current_status = self.status.overall_status
1221-
elapsed = time.time() - start_time
1222-
print(f"Current status: {current_status} (Elapsed: {elapsed:.1f}s)")
1223-
1224-
if target_status == current_status:
1225-
logger.info(f"Final Resource Status: {current_status}")
1226-
return
1227-
1228-
if "failed" in current_status.lower():
1229-
from sagemaker.core.utils.exceptions import FailedStatusError
1230-
raise FailedStatusError(
1231-
resource_type="PipelineExecution",
1232-
status=current_status,
1233-
reason=self.status.failure_reason,
1234-
)
1235-
1236-
if timeout is not None and elapsed >= timeout:
1237-
from sagemaker.core.utils.exceptions import TimeoutExceededError
1238-
raise TimeoutExceededError(
1239-
resource_type="EvaluationJob",
1240-
status=current_status,
1241-
message="Your evaluation job is still running. Call .refresh() to check its current status.",
1242-
)
1243-
1244-
time.sleep(poll)
1164+
# Terminal experience - plain print matching training format
1165+
pipeline_name = None
1166+
if self.arn:
1167+
arn_parts = self.arn.split('/')
1168+
if len(arn_parts) >= 3:
1169+
pipeline_name = arn_parts[-3]
1170+
print(f"\nEvaluation started: {self.name}", flush=True)
1171+
if pipeline_name:
1172+
print(f"Pipeline: {pipeline_name}", flush=True)
1173+
print(f"Execution ARN: {self.arn}", flush=True)
1174+
1175+
while True:
1176+
self.refresh()
1177+
current_status = self.status.overall_status
1178+
elapsed = time.time() - start_time
1179+
1180+
# Print step transitions (matching training format)
1181+
if self.status.step_details:
1182+
print("\n--------------------------------------\n", flush=True)
1183+
print("Status Transitions:", flush=True)
1184+
for step in self.status.step_details:
1185+
duration = ""
1186+
check = ""
1187+
if step.start_time and step.end_time:
1188+
try:
1189+
from datetime import datetime
1190+
start_dt = datetime.fromisoformat(step.start_time.replace('Z', '+00:00'))
1191+
end_dt = datetime.fromisoformat(step.end_time.replace('Z', '+00:00'))
1192+
duration = f"({(end_dt - start_dt).total_seconds():.1f}s)"
1193+
except Exception:
1194+
pass
1195+
check = "✓"
1196+
elif step.start_time:
1197+
try:
1198+
from datetime import datetime, timezone
1199+
start_dt = datetime.fromisoformat(step.start_time.replace('Z', '+00:00'))
1200+
running_secs = (datetime.now(timezone.utc) - start_dt).total_seconds()
1201+
duration = f"(Running... {running_secs:.0f}s)"
1202+
except Exception:
1203+
duration = "(Running...)"
1204+
check = "⋯"
1205+
1206+
step_msg = f" {check} {step.display_name or step.name}: {step.status} {duration}"
1207+
print(step_msg, flush=True)
1208+
if step.job_arn and ('executing' in step.status.lower() or 'failed' in step.status.lower()):
1209+
print(f" Job ARN: {step.job_arn}", flush=True)
1210+
1211+
print(f"\nStatus: {current_status} (Elapsed: {elapsed:.1f}s)", flush=True)
1212+
1213+
if target_status == current_status:
1214+
if self.s3_output_path:
1215+
print(f"\nResults S3: {self.s3_output_path}", flush=True)
1216+
return
1217+
1218+
if "failed" in current_status.lower():
1219+
if self.status.failure_reason:
1220+
print(f"\nFailure reason: {self.status.failure_reason}", flush=True)
1221+
if self.status.step_details:
1222+
for step in self.status.step_details:
1223+
if 'failed' in step.status.lower():
1224+
print(f"\nFailed step: {step.display_name or step.name}", flush=True)
1225+
if step.failure_reason:
1226+
print(f"Failure reason: {step.failure_reason}", flush=True)
1227+
if step.job_arn:
1228+
print(f"Job ARN: {step.job_arn}", flush=True)
1229+
job_name = step.job_arn.split('/')[-1] if '/' in step.job_arn else ''
1230+
if job_name:
1231+
# Derive log group from ARN type
1232+
if 'training-job/' in step.job_arn:
1233+
log_group = "/aws/sagemaker/TrainingJobs"
1234+
elif 'processing-job/' in step.job_arn:
1235+
log_group = "/aws/sagemaker/ProcessingJobs"
1236+
elif 'transform-job/' in step.job_arn:
1237+
log_group = "/aws/sagemaker/TransformJobs"
1238+
else:
1239+
log_group = "/aws/sagemaker/TrainingJobs"
1240+
print(f"Log group: {log_group}", flush=True)
1241+
print(f"Log stream prefix: {job_name}", flush=True)
1242+
from sagemaker.train.common_utils.metrics_visualizer import get_cloudwatch_logs_url
1243+
cw_url = get_cloudwatch_logs_url(step.job_arn)
1244+
if cw_url:
1245+
print(f"CloudWatch Logs: {cw_url}", flush=True)
1246+
from sagemaker.core.utils.exceptions import FailedStatusError
1247+
raise FailedStatusError(
1248+
resource_type="PipelineExecution",
1249+
status=current_status,
1250+
reason=self.status.failure_reason,
1251+
)
1252+
1253+
if timeout is not None and time.time() - start_time >= timeout:
1254+
from sagemaker.core.utils.exceptions import TimeoutExceededError
1255+
raise TimeoutExceededError(
1256+
resource_type="EvaluationJob",
1257+
status=current_status,
1258+
message="Your evaluation job is still running. Call .refresh() to check its current status.",
1259+
)
1260+
1261+
time.sleep(poll)
12451262

12461263
def _enrich_with_step_details(
12471264
self,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Tests for eval pipeline observability prints in terminal mode."""
2+
from unittest.mock import patch, MagicMock
3+
4+
import pytest
5+
6+
from sagemaker.train.evaluate.execution import (
7+
EvaluationPipelineExecution,
8+
PipelineExecutionStatus,
9+
StepDetail,
10+
)
11+
12+
13+
def _make_execution(status="Succeeded", step_details=None, failure_reason=None, s3_output_path=None):
14+
exec_obj = EvaluationPipelineExecution(
15+
name="benchmark-eval-mmlu",
16+
arn="arn:aws:sagemaker:us-west-2:123456789:pipeline/sm-eval-benchmark-abc/execution/exec-123",
17+
status=PipelineExecutionStatus(
18+
overall_status=status,
19+
step_details=step_details or [],
20+
failure_reason=failure_reason,
21+
),
22+
s3_output_path=s3_output_path,
23+
)
24+
exec_obj._pipeline_execution = MagicMock()
25+
return exec_obj
26+
27+
28+
class TestEvalObservabilityAtStart:
29+
@patch("sagemaker.train.evaluate.execution.time.sleep")
30+
@patch.object(EvaluationPipelineExecution, "refresh")
31+
def test_prints_pipeline_info_at_start(self, mock_refresh, mock_sleep, capsys):
32+
exec_obj = _make_execution(status="Succeeded")
33+
exec_obj.wait(poll=0, timeout=1)
34+
captured = capsys.readouterr()
35+
assert "Evaluation started: benchmark-eval-mmlu" in captured.out
36+
assert "Pipeline: sm-eval-benchmark-abc" in captured.out
37+
assert "Execution ARN:" in captured.out
38+
39+
40+
class TestEvalObservabilityStepTransitions:
41+
@patch("sagemaker.train.evaluate.execution.time.sleep")
42+
@patch.object(EvaluationPipelineExecution, "refresh")
43+
def test_prints_step_transitions(self, mock_refresh, mock_sleep, capsys):
44+
steps = [
45+
StepDetail(name="EvaluateBaseModel", status="Succeeded", display_name="EvaluateBaseModel",
46+
start_time="2026-01-01T00:00:00Z", end_time="2026-01-01T00:01:00Z",
47+
job_arn="arn:aws:sagemaker:us-west-2:123456789:training-job/eval-base-xyz"),
48+
]
49+
exec_obj = _make_execution(status="Succeeded", step_details=steps)
50+
exec_obj.wait(poll=0, timeout=1)
51+
captured = capsys.readouterr()
52+
assert "✓ EvaluateBaseModel: Succeeded" in captured.out
53+
assert "(60.0s)" in captured.out
54+
55+
@patch("sagemaker.train.evaluate.execution.time.sleep")
56+
@patch.object(EvaluationPipelineExecution, "refresh")
57+
def test_prints_job_arn_for_executing_step(self, mock_refresh, mock_sleep, capsys):
58+
steps = [
59+
StepDetail(name="EvaluateCustomModel", status="Executing", display_name="EvaluateCustomModel",
60+
start_time="2026-01-01T00:00:00Z",
61+
job_arn="arn:aws:sagemaker:us-west-2:123456789:training-job/eval-custom-xyz"),
62+
]
63+
# First poll shows Executing, then Succeeded
64+
call_count = [0]
65+
def side_effect():
66+
call_count[0] += 1
67+
if call_count[0] > 1:
68+
exec_obj.status.overall_status = "Succeeded"
69+
exec_obj.status.step_details[0].status = "Succeeded"
70+
exec_obj.status.step_details[0].end_time = "2026-01-01T00:01:00Z"
71+
exec_obj = _make_execution(status="Executing", step_details=steps)
72+
mock_refresh.side_effect = side_effect
73+
exec_obj.wait(poll=0, timeout=5)
74+
captured = capsys.readouterr()
75+
assert "Job ARN: arn:aws:sagemaker:us-west-2:123456789:training-job/eval-custom-xyz" in captured.out
76+
77+
78+
class TestEvalObservabilityOnSuccess:
79+
@patch("sagemaker.train.evaluate.execution.time.sleep")
80+
@patch.object(EvaluationPipelineExecution, "refresh")
81+
def test_prints_s3_output_on_success(self, mock_refresh, mock_sleep, capsys):
82+
exec_obj = _make_execution(status="Succeeded", s3_output_path="s3://bucket/eval-results/")
83+
exec_obj.wait(poll=0, timeout=1)
84+
captured = capsys.readouterr()
85+
assert "Results S3: s3://bucket/eval-results/" in captured.out
86+
87+
88+
class TestEvalObservabilityOnFailure:
89+
@patch("sagemaker.train.evaluate.execution.time.sleep")
90+
@patch.object(EvaluationPipelineExecution, "refresh")
91+
def test_prints_failed_step_info(self, mock_refresh, mock_sleep, capsys):
92+
steps = [
93+
StepDetail(name="EvaluateCustomModel", status="Failed",
94+
display_name="EvaluateCustomModel",
95+
failure_reason="ResourceLimitExceeded",
96+
job_arn="arn:aws:sagemaker:us-west-2:123456789:training-job/eval-custom-xyz"),
97+
]
98+
exec_obj = _make_execution(status="Failed", step_details=steps, failure_reason="Step failed")
99+
with pytest.raises(Exception):
100+
exec_obj.wait(poll=0, timeout=1)
101+
captured = capsys.readouterr()
102+
assert "Failed step: EvaluateCustomModel" in captured.out
103+
assert "ResourceLimitExceeded" in captured.out
104+
assert "Job ARN: arn:aws:sagemaker:us-west-2:123456789:training-job/eval-custom-xyz" in captured.out
105+
assert "Log group: /aws/sagemaker/TrainingJobs" in captured.out
106+
assert "Log stream prefix: eval-custom-xyz" in captured.out
107+
assert "CloudWatch Logs:" in captured.out

0 commit comments

Comments
 (0)