Skip to content

Commit fa8aa2c

Browse files
committed
fix(apps): warn when gpu pins fall back to the static pool table
a failed catalog lookup was logged at debug and cached for the process, so pins silently lost exclusions the static table lacks. warn instead, and retry the catalog after a minute.
1 parent 1ffb5a0 commit fa8aa2c

2 files changed

Lines changed: 38 additions & 7 deletions

File tree

‎runpod/apps/gpu.py‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
configured. CpuInstanceType values are backend cpu flavor ids.
66
"""
77

8-
import functools
98
import logging
109
import os
10+
import time
1111
from enum import Enum
12-
from typing import Dict, List, Optional, Set, Union
12+
from typing import Any, Dict, List, Optional, Set, Union
1313

1414
log = logging.getLogger(__name__)
1515

@@ -216,18 +216,29 @@ def resolve_gpu_string(value: str) -> List[str]:
216216
CATALOG_URL = "https://api.runpod.io/v2/catalog/gpus"
217217

218218

219-
@functools.lru_cache(maxsize=1)
219+
# live catalog result, or when it last failed
220+
_catalog: Dict[str, Any] = {}
221+
_CATALOG_RETRY_SECONDS = 60.0
222+
223+
220224
def live_pool_members() -> Optional[Dict[str, Set[str]]]:
221225
"""pool id -> device ids, from the platform's gpu catalog.
222226
223227
the static POOLS_TO_TYPES table lags the platform (pools gain mig
224228
slices and new skus), so exact-device pinning reads the live
225-
membership. returns None when the catalog is unreachable.
229+
membership. returns None when the catalog is unreachable. a result
230+
is kept for the process; a failure is retried after a minute.
226231
"""
227232
import requests
228233

229234
from .utils.network import api_key
230235

236+
if _catalog.get("members") is not None:
237+
return _catalog["members"]
238+
failed_at = _catalog.get("failed_at")
239+
if failed_at is not None and time.monotonic() - failed_at < _CATALOG_RETRY_SECONDS:
240+
return None
241+
231242
url = os.environ.get("RUNPOD_GPU_CATALOG_URL", CATALOG_URL)
232243
try:
233244
resp = requests.get(
@@ -240,12 +251,23 @@ def live_pool_members() -> Optional[Dict[str, Set[str]]]:
240251
for gpu in gpus:
241252
if gpu.get("pool") and gpu.get("id"):
242253
members.setdefault(gpu["pool"], set()).add(gpu["id"])
243-
return members or None
254+
if not members:
255+
raise ValueError("catalog lists no pools")
256+
_catalog["members"] = members
257+
return members
244258
except Exception as exc: # any failure falls back to the static table
245-
log.debug("gpu catalog unavailable (%s); using the static pool table", exc)
259+
_catalog["failed_at"] = time.monotonic()
260+
log.warning(
261+
"gpu catalog unavailable (%s); device pins use the static pool "
262+
"table, which can miss newer devices in a pool",
263+
exc,
264+
)
246265
return None
247266

248267

268+
live_pool_members.cache_clear = _catalog.clear # type: ignore[attr-defined]
269+
270+
249271
def _pool_members(pool: str) -> Set[str]:
250272
live = live_pool_members()
251273
if live is not None and pool in live:

‎tests/test_apps/test_app.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ def test_gpu_ids_falls_back_to_static_pools(self, monkeypatch):
427427
def test_live_pool_members_parses_catalog_and_fails_soft(self, monkeypatch):
428428
from unittest.mock import MagicMock
429429

430+
from runpod.apps import gpu as gpu_mod
431+
430432
monkeypatch.setenv("RUNPOD_API_KEY", "k")
431433
resp = MagicMock()
432434
resp.json.return_value = {
@@ -441,8 +443,15 @@ def test_live_pool_members_parses_catalog_and_fails_soft(self, monkeypatch):
441443
assert fetch() == {"AMPERE_24": {"NVIDIA L4"}}
442444
assert get.call_args.kwargs["headers"]["Authorization"] == "Bearer k"
443445
fetch.cache_clear()
444-
with patch("requests.get", side_effect=OSError("offline")):
446+
with patch("requests.get", side_effect=OSError("offline")) as get:
445447
assert fetch() is None
448+
# a failure is not retried on every lookup
449+
assert fetch() is None
450+
assert get.call_count == 1
451+
# it is retried once the window passes
452+
gpu_mod._catalog["failed_at"] -= gpu_mod._CATALOG_RETRY_SECONDS
453+
with patch("requests.get", return_value=resp):
454+
assert fetch() == {"AMPERE_24": {"NVIDIA L4"}}
446455
fetch.cache_clear()
447456

448457
def test_gpu_ids_maps_devices_to_pools(self):

0 commit comments

Comments
 (0)