XiangpengYang commited on
Commit
b24bc66
·
1 Parent(s): 5b21b68

feat: add lazy pi05 policy lifecycle

Browse files
Files changed (2) hide show
  1. model_loader.py +101 -0
  2. tests/test_model_loader.py +44 -0
model_loader.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Thread-safe lazy lifecycle for the heavyweight π₀.₅ UR policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gc
6
+ from pathlib import Path
7
+ import sys
8
+ import threading
9
+ from collections.abc import Callable
10
+
11
+ from artifacts import (
12
+ download_checkpoint,
13
+ normalize_checkpoint_path,
14
+ normalize_model_id,
15
+ )
16
+
17
+
18
+ class ModelUnavailableError(RuntimeError):
19
+ """Raised when the requested policy cannot be initialized."""
20
+
21
+
22
+ def _release_gpu_memory() -> None:
23
+ gc.collect()
24
+ try:
25
+ import torch
26
+
27
+ if torch.cuda.is_available():
28
+ torch.cuda.empty_cache()
29
+ except ImportError:
30
+ pass
31
+
32
+
33
+ class ModelManager:
34
+ def __init__(self, loader: Callable[[str, str], object] | None = None):
35
+ self._loader = loader or self._load_default
36
+ self._lock = threading.Lock()
37
+ self._value = None
38
+ self._active_key: tuple[str, str] | None = None
39
+ self._error: str | None = None
40
+
41
+ @property
42
+ def active_key(self) -> tuple[str, str] | None:
43
+ return self._active_key
44
+
45
+ @property
46
+ def health_message(self) -> str:
47
+ if self._value is not None and self._active_key is not None:
48
+ model_id, checkpoint_path = self._active_key
49
+ return f"Model ready: {model_id}/{checkpoint_path}."
50
+ if self._error:
51
+ return f"Model unavailable: {self._error}"
52
+ return "Model has not been loaded yet."
53
+
54
+ def get(self, model_id: str, checkpoint_path: str):
55
+ key = (
56
+ normalize_model_id(model_id),
57
+ normalize_checkpoint_path(checkpoint_path),
58
+ )
59
+ if self._value is not None and self._active_key == key:
60
+ return self._value
61
+ with self._lock:
62
+ if self._value is not None and self._active_key == key:
63
+ return self._value
64
+ if self._value is not None:
65
+ self._value = None
66
+ self._active_key = None
67
+ _release_gpu_memory()
68
+ self._error = None
69
+ try:
70
+ value = self._loader(*key)
71
+ except Exception as exc:
72
+ _release_gpu_memory()
73
+ detail = str(exc) or exc.__class__.__name__
74
+ self._error = f"{key[0]}/{key[1]}: {detail}"
75
+ raise ModelUnavailableError(self._error) from exc
76
+ self._value = value
77
+ self._active_key = key
78
+ return value
79
+
80
+ @staticmethod
81
+ def _load_default(model_id: str, checkpoint_path: str):
82
+ import torch
83
+
84
+ if not torch.cuda.is_available():
85
+ raise RuntimeError("CUDA GPU is required for π₀.₅ inference")
86
+ runtime = str(Path(__file__).resolve().parent / "openpi_runtime")
87
+ if runtime not in sys.path:
88
+ sys.path.insert(0, runtime)
89
+ from openpi.policies import policy_config
90
+ from openpi.training import config as openpi_config
91
+
92
+ paths = download_checkpoint(model_id, checkpoint_path)
93
+ config = openpi_config.get_config("pi05_ur_demo_state")
94
+ return policy_config.create_trained_policy(
95
+ config,
96
+ paths.checkpoint,
97
+ pytorch_device="cuda",
98
+ )
99
+
100
+
101
+ MODEL_MANAGER = ModelManager()
tests/test_model_loader.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+
4
+ class ModelManagerTests(unittest.TestCase):
5
+ def test_manager_caches_same_key_and_replaces_changed_key(self):
6
+ from model_loader import ModelManager
7
+
8
+ calls = []
9
+ manager = ModelManager(
10
+ loader=lambda model, path: calls.append((model, path)) or object()
11
+ )
12
+ first = manager.get("owner/model", "a")
13
+ self.assertIs(manager.get("owner/model", "a"), first)
14
+ second = manager.get("owner/model", "b")
15
+ self.assertIsNot(second, first)
16
+ self.assertEqual(calls, [("owner/model", "a"), ("owner/model", "b")])
17
+
18
+ def test_failed_load_is_reported_and_can_retry(self):
19
+ from model_loader import ModelManager, ModelUnavailableError
20
+
21
+ attempts = 0
22
+
23
+ def loader(*_):
24
+ nonlocal attempts
25
+ attempts += 1
26
+ raise RuntimeError("bad checkpoint")
27
+
28
+ manager = ModelManager(loader=loader)
29
+ for _ in range(2):
30
+ with self.assertRaisesRegex(ModelUnavailableError, "bad checkpoint"):
31
+ manager.get("owner/model", "a")
32
+ self.assertEqual(attempts, 2)
33
+ self.assertIn("bad checkpoint", manager.health_message)
34
+
35
+ def test_invalid_identity_is_rejected_before_loader(self):
36
+ from model_loader import ModelManager
37
+
38
+ manager = ModelManager(loader=lambda *_: self.fail("loader was called"))
39
+ with self.assertRaises(ValueError):
40
+ manager.get("", "checkpoint")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ unittest.main()