Skip to content

Commit 1d14fa8

Browse files
ko3n1gclaude
andcommitted
fix: treat DGXCloud UNKNOWN/transient status as PENDING to avoid false failures
When a job is submitted to DGXCloud, the API may transiently return a non-200 response or an "Unknown" phase before the workload is fully registered. Previously this was mapped to AppState.FAILED, causing wait_and_exit() to treat the job as terminated immediately while the pod was still starting up on the cluster. - DGXCloudState.UNKNOWN now maps to AppState.PENDING in DGX_STATES - executor.status() returns None (instead of DGXCloudState.UNKNOWN) on non-200 HTTP responses so transient API errors don't look like a real "Unknown" phase reported by the scheduler - describe() fallback for unknown keys in DGX_STATES changed to PENDING - Tests updated and added to cover all three code paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: oliver könig <okoenig@nvidia.com>
1 parent 611ae61 commit 1d14fa8

4 files changed

Lines changed: 65 additions & 7 deletions

File tree

nemo_run/core/execution/dgxcloud.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,11 @@ def status(self, job_id: str) -> Optional[DGXCloudState]:
369369
headers = self._default_headers(token=token)
370370
response = requests.get(url, headers=headers)
371371
if response.status_code != 200:
372-
return DGXCloudState("Unknown")
372+
logger.warning(
373+
f"Failed to get status for job {job_id}, "
374+
f"status_code={response.status_code}. Treating as transient."
375+
)
376+
return None
373377

374378
r_json = response.json()
375379
return DGXCloudState(r_json["phase"])

nemo_run/run/torchx_backend/schedulers/dgxcloud.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
DGXCloudState.FAILED: AppState.FAILED,
6060
DGXCloudState.COMPLETED: AppState.SUCCEEDED,
6161
DGXCloudState.TERMINATING: AppState.RUNNING,
62-
DGXCloudState.UNKNOWN: AppState.FAILED,
62+
DGXCloudState.UNKNOWN: AppState.PENDING,
6363
}
6464

6565
log = logging.getLogger(__name__)
@@ -192,7 +192,7 @@ def describe(self, app_id: str) -> Optional[DescribeAppResponse]:
192192
return None
193193

194194
dgx_state = executor.status(job_id) or DGXCloudState.UNKNOWN
195-
app_state = DGX_STATES.get(dgx_state, AppState.UNKNOWN)
195+
app_state = DGX_STATES.get(dgx_state, AppState.PENDING)
196196
roles_statuses[0].replicas[0].state = app_state
197197

198198
return DescribeAppResponse(

test/core/execution/test_dgxcloud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ def test_status_error_response(self, mock_get):
936936

937937
status = executor.status("job123")
938938

939-
assert status == DGXCloudState.UNKNOWN
939+
assert status is None
940940

941941
@patch("requests.get")
942942
def test_cancel(self, mock_get):

test/run/torchx_backend/schedulers/test_dgxcloud.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919

2020
import pytest
2121
from torchx.schedulers.api import AppDryRunInfo
22-
from torchx.specs import AppDef, Role
22+
from torchx.specs import AppDef, AppState, Role
2323

24-
from nemo_run.core.execution.dgxcloud import DGXCloudExecutor
25-
from nemo_run.run.torchx_backend.schedulers.dgxcloud import DGXCloudScheduler, create_scheduler
24+
from nemo_run.core.execution.dgxcloud import DGXCloudExecutor, DGXCloudState
25+
from nemo_run.run.torchx_backend.schedulers.dgxcloud import (
26+
DGX_STATES,
27+
DGXCloudScheduler,
28+
create_scheduler,
29+
)
2630

2731

2832
@pytest.fixture
@@ -184,6 +188,56 @@ def test_log_iter(dgx_cloud_scheduler, dgx_cloud_executor):
184188
assert logs == ["log2", "log3"]
185189

186190

191+
def test_unknown_state_maps_to_pending_not_failed():
192+
# DGXCloudState.UNKNOWN must map to PENDING so transient API errors during
193+
# job startup do not cause wait_and_exit() to treat the job as terminal.
194+
assert DGX_STATES[DGXCloudState.UNKNOWN] == AppState.PENDING
195+
196+
197+
def test_describe_returns_pending_when_status_is_none(dgx_cloud_scheduler, dgx_cloud_executor):
198+
# Regression test: executor.status() returns None when the auth token is
199+
# missing or the API call fails transiently right after job submission.
200+
# describe() must return PENDING so the wait loop keeps polling.
201+
with (
202+
mock.patch(
203+
"nemo_run.run.torchx_backend.schedulers.dgxcloud._get_job_dirs"
204+
) as mock_get_job_dirs,
205+
mock.patch.object(DGXCloudExecutor, "status", return_value=None),
206+
):
207+
mock_get_job_dirs.return_value = {
208+
"test_experiment___test_role___test_job_id": {
209+
"job_status": "RUNNING",
210+
"executor": dgx_cloud_executor,
211+
}
212+
}
213+
214+
response = dgx_cloud_scheduler.describe("test_experiment___test_role___test_job_id")
215+
assert response is not None
216+
assert response.state == AppState.PENDING
217+
218+
219+
def test_describe_returns_pending_when_status_is_unknown(dgx_cloud_scheduler, dgx_cloud_executor):
220+
# Regression test: the DGXCloud API transiently returns "Unknown" before a
221+
# job is visible (e.g. HTTP 404 right after submission). describe() must
222+
# return PENDING so the wait loop keeps polling instead of failing.
223+
with (
224+
mock.patch(
225+
"nemo_run.run.torchx_backend.schedulers.dgxcloud._get_job_dirs"
226+
) as mock_get_job_dirs,
227+
mock.patch.object(DGXCloudExecutor, "status", return_value=DGXCloudState.UNKNOWN),
228+
):
229+
mock_get_job_dirs.return_value = {
230+
"test_experiment___test_role___test_job_id": {
231+
"job_status": "RUNNING",
232+
"executor": dgx_cloud_executor,
233+
}
234+
}
235+
236+
response = dgx_cloud_scheduler.describe("test_experiment___test_role___test_job_id")
237+
assert response is not None
238+
assert response.state == AppState.PENDING
239+
240+
187241
def test_log_iter_str(dgx_cloud_scheduler, dgx_cloud_executor):
188242
with mock.patch(
189243
"nemo_run.run.torchx_backend.schedulers.dgxcloud._get_job_dirs"

0 commit comments

Comments
 (0)