lingangu commited on
Commit
259de4c
·
1 Parent(s): 7f618bf

v1.1: feats a spec model

Browse files
Files changed (5) hide show
  1. README.md +13 -3
  2. app.py +21 -4
  3. artifacts.py +19 -4
  4. model_loader.py +2 -0
  5. tests/test_artifacts.py +15 -0
README.md CHANGED
@@ -38,9 +38,13 @@ The selected directory must contain either `params/` (JAX checkpoint) or
38
  `model.safetensors` (PyTorch checkpoint), plus the training statistics at:
39
 
40
  ```text
41
- assets/ur_demo/norm_stats.json
42
  ```
43
 
 
 
 
 
44
  Use a Space secret named `HF_TOKEN` when the model repository is private.
45
 
46
  ## Inputs and outputs
@@ -64,8 +68,12 @@ radians, matching the collected dataset.
64
  ## Deploy
65
 
66
  Create a Hugging Face Gradio Space with a CUDA GPU and push this repository.
67
- Model download and initialization happen lazily on the first prediction. Only
68
- one inference request runs at a time to protect GPU memory.
 
 
 
 
69
 
70
  For local use with all dependencies installed:
71
 
@@ -89,3 +97,5 @@ pytest tests/test_gpu_smoke.py -v
89
 
90
  Do not put a Hugging Face access token in this command; use `HF_TOKEN` as a
91
  local environment secret or a Hugging Face Space secret.
 
 
 
38
  `model.safetensors` (PyTorch checkpoint), plus the training statistics at:
39
 
40
  ```text
41
+ assets/**/norm_stats.json
42
  ```
43
 
44
+ The Space also accepts a root-level `norm_stats.json`. The asset directory name
45
+ is not required to be `ur_demo`; this supports checkpoints exported with the
46
+ original dataset or robot name, such as `assets/F-Fer/ur-1/norm_stats.json`.
47
+
48
  Use a Space secret named `HF_TOKEN` when the model repository is private.
49
 
50
  ## Inputs and outputs
 
68
  ## Deploy
69
 
70
  Create a Hugging Face Gradio Space with a CUDA GPU and push this repository.
71
+ When `PI05_MODEL_ID` and `PI05_CHECKPOINT_PATH` are configured as Space
72
+ variables, the checkpoint is downloaded and validated during app startup,
73
+ before the GPU-decorated prediction call. Model initialization remains lazy on
74
+ the first prediction. If those variables are left empty, download falls back
75
+ to the first prediction. Only one inference request runs at a time to protect
76
+ GPU memory.
77
 
78
  For local use with all dependencies installed:
79
 
 
97
 
98
  Do not put a Hugging Face access token in this command; use `HF_TOKEN` as a
99
  local environment secret or a Hugging Face Space secret.
100
+
101
+ 哈基米
app.py CHANGED
@@ -19,11 +19,21 @@ except ImportError: # Local and dedicated-GPU environments omit this helper.
19
 
20
  spaces = _SpacesFallback()
21
 
22
- from artifacts import resolve_checkpoint_path, resolve_model_id
23
  from inference import ACTION_LABELS, run_prediction
24
  from model_loader import DEFAULT_POLICY_CONFIG, MODEL_MANAGER, POLICY_CONFIGS
25
 
26
 
 
 
 
 
 
 
 
 
 
 
27
  def _gradio_integer(value, name: str) -> int:
28
  if isinstance(value, bool) or not isinstance(value, (int, float)):
29
  raise ValueError(f"{name} must be an integer")
@@ -89,6 +99,7 @@ def build_demo():
89
  with gr.Blocks(title="π₀.₅ UR Action Predictor") as demo:
90
  gr.Markdown(
91
  "# π₀.₅ UR Action Predictor\n"
 
92
  "Upload the fixed and wrist camera views, enter the current TCP/gripper "
93
  "state and a task instruction. This demo predicts actions only and does "
94
  "not directly control a robot."
@@ -127,15 +138,14 @@ def build_demo():
127
  tcp_y = gr.Number(value=0.0, label="TCP y")
128
  tcp_z = gr.Number(value=0.0, label="TCP z")
129
  tcp_roll = gr.Number(value=0.0, label="TCP roll")
 
130
  with gr.Row():
131
  tcp_pitch = gr.Number(value=0.0, label="TCP pitch")
132
  tcp_yaw = gr.Number(value=0.0, label="TCP yaw")
133
  gripper = gr.Number(value=0.0, label="Gripper")
134
  trial_index = gr.Number(value=0, precision=0, minimum=0, label="Trial index")
135
  predict_button = gr.Button("Predict actions", variant="primary")
136
- status = gr.Markdown(
137
- "The model loads on the first prediction; download and initialization may take several minutes."
138
- )
139
  actions = gr.Dataframe(headers=list(ACTION_LABELS), interactive=False, label="Predicted actions")
140
  json_output = gr.File(label="Download JSON result")
141
  predict_button.click(
@@ -161,6 +171,13 @@ def build_demo():
161
  return demo
162
 
163
 
 
 
 
 
 
 
 
164
  demo = build_demo()
165
 
166
 
 
19
 
20
  spaces = _SpacesFallback()
21
 
22
+ from artifacts import download_checkpoint, resolve_checkpoint_path, resolve_model_id
23
  from inference import ACTION_LABELS, run_prediction
24
  from model_loader import DEFAULT_POLICY_CONFIG, MODEL_MANAGER, POLICY_CONFIGS
25
 
26
 
27
+ def prefetch_configured_checkpoint() -> str:
28
+ """Download and validate configured weights during Space startup, before GPU use."""
29
+ model_id = resolve_model_id()
30
+ if not model_id:
31
+ return "No PI05_MODEL_ID configured; download will occur on first prediction."
32
+ checkpoint_path = resolve_checkpoint_path()
33
+ paths = download_checkpoint(model_id, checkpoint_path)
34
+ return f"Checkpoint ready: {model_id}/{checkpoint_path} ({paths.norm_stats.name})."
35
+
36
+
37
  def _gradio_integer(value, name: str) -> int:
38
  if isinstance(value, bool) or not isinstance(value, (int, float)):
39
  raise ValueError(f"{name} must be an integer")
 
99
  with gr.Blocks(title="π₀.₅ UR Action Predictor") as demo:
100
  gr.Markdown(
101
  "# π₀.₅ UR Action Predictor\n"
102
+ "forked from ![XiangpengYang](https://huggingface.co/spaces/XiangpengYang/pi0.5), thx!"
103
  "Upload the fixed and wrist camera views, enter the current TCP/gripper "
104
  "state and a task instruction. This demo predicts actions only and does "
105
  "not directly control a robot."
 
138
  tcp_y = gr.Number(value=0.0, label="TCP y")
139
  tcp_z = gr.Number(value=0.0, label="TCP z")
140
  tcp_roll = gr.Number(value=0.0, label="TCP roll")
141
+ gr.Markdown("哈基米")
142
  with gr.Row():
143
  tcp_pitch = gr.Number(value=0.0, label="TCP pitch")
144
  tcp_yaw = gr.Number(value=0.0, label="TCP yaw")
145
  gripper = gr.Number(value=0.0, label="Gripper")
146
  trial_index = gr.Number(value=0, precision=0, minimum=0, label="Trial index")
147
  predict_button = gr.Button("Predict actions", variant="primary")
148
+ status = gr.Markdown(STARTUP_STATUS)
 
 
149
  actions = gr.Dataframe(headers=list(ACTION_LABELS), interactive=False, label="Predicted actions")
150
  json_output = gr.File(label="Download JSON result")
151
  predict_button.click(
 
171
  return demo
172
 
173
 
174
+ # Hugging Face Spaces imports app.py during startup. Prefetching here moves the
175
+ # multi-GB download out of the GPU-decorated prediction request when env vars are set.
176
+ try:
177
+ STARTUP_STATUS = prefetch_configured_checkpoint()
178
+ except Exception as exc:
179
+ STARTUP_STATUS = f"Checkpoint prefetch deferred: {exc}"
180
+
181
  demo = build_demo()
182
 
183
 
artifacts.py CHANGED
@@ -13,6 +13,7 @@ DEFAULT_CHECKPOINT_PATH = "checkpoint"
13
  @dataclass(frozen=True)
14
  class ArtifactPaths:
15
  checkpoint: Path
 
16
 
17
 
18
  def snapshot_download(**kwargs) -> str:
@@ -49,6 +50,22 @@ def resolve_checkpoint_path() -> str:
49
  return os.getenv("PI05_CHECKPOINT_PATH", DEFAULT_CHECKPOINT_PATH)
50
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  def download_checkpoint(model_id: str, checkpoint_path: str) -> ArtifactPaths:
53
  model_id = normalize_model_id(model_id)
54
  relative = normalize_checkpoint_path(checkpoint_path)
@@ -58,7 +75,5 @@ def download_checkpoint(model_id: str, checkpoint_path: str) -> ArtifactPaths:
58
  raise FileNotFoundError(
59
  f"checkpoint has neither params/ nor model.safetensors: {checkpoint}"
60
  )
61
- statistics = checkpoint / "assets/ur_demo/norm_stats.json"
62
- if not statistics.is_file():
63
- raise FileNotFoundError(f"UR normalization statistics not found: {statistics}")
64
- return ArtifactPaths(checkpoint=checkpoint)
 
13
  @dataclass(frozen=True)
14
  class ArtifactPaths:
15
  checkpoint: Path
16
+ norm_stats: Path
17
 
18
 
19
  def snapshot_download(**kwargs) -> str:
 
50
  return os.getenv("PI05_CHECKPOINT_PATH", DEFAULT_CHECKPOINT_PATH)
51
 
52
 
53
+ def _find_norm_stats(checkpoint: Path) -> Path:
54
+ """Find checkpoint statistics without assuming the training repo name."""
55
+ preferred = checkpoint / "assets/ur_demo/norm_stats.json"
56
+ if preferred.is_file():
57
+ return preferred
58
+ candidates = sorted((checkpoint / "assets").rglob("norm_stats.json"))
59
+ if candidates:
60
+ return candidates[0]
61
+ root_stats = checkpoint / "norm_stats.json"
62
+ if root_stats.is_file():
63
+ return root_stats
64
+ raise FileNotFoundError(
65
+ f"UR normalization statistics not found under {checkpoint / 'assets'} or at {root_stats}"
66
+ )
67
+
68
+
69
  def download_checkpoint(model_id: str, checkpoint_path: str) -> ArtifactPaths:
70
  model_id = normalize_model_id(model_id)
71
  relative = normalize_checkpoint_path(checkpoint_path)
 
75
  raise FileNotFoundError(
76
  f"checkpoint has neither params/ nor model.safetensors: {checkpoint}"
77
  )
78
+ statistics = _find_norm_stats(checkpoint)
79
+ return ArtifactPaths(checkpoint=checkpoint, norm_stats=statistics)
 
 
model_loader.py CHANGED
@@ -98,6 +98,7 @@ class ModelManager:
98
  if runtime not in sys.path:
99
  sys.path.insert(0, runtime)
100
  from openpi.policies import policy_config
 
101
  from openpi.training import config as openpi_config
102
 
103
  paths = download_checkpoint(model_id, checkpoint_path)
@@ -105,6 +106,7 @@ class ModelManager:
105
  return policy_config.create_trained_policy(
106
  config,
107
  paths.checkpoint,
 
108
  pytorch_device="cuda",
109
  )
110
 
 
98
  if runtime not in sys.path:
99
  sys.path.insert(0, runtime)
100
  from openpi.policies import policy_config
101
+ from openpi.shared import normalize
102
  from openpi.training import config as openpi_config
103
 
104
  paths = download_checkpoint(model_id, checkpoint_path)
 
106
  return policy_config.create_trained_policy(
107
  config,
108
  paths.checkpoint,
109
+ norm_stats=normalize.load(paths.norm_stats.parent),
110
  pytorch_device="cuda",
111
  )
112
 
tests/test_artifacts.py CHANGED
@@ -24,6 +24,21 @@ class ArtifactTests(unittest.TestCase):
24
  with mock.patch.object(artifacts, "snapshot_download", return_value=str(root)):
25
  paths = artifacts.download_checkpoint("owner/model", "checkpoints/30000")
26
  self.assertEqual(paths.checkpoint, checkpoint)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  def test_download_checkpoint_reports_missing_weights(self):
29
  import artifacts
 
24
  with mock.patch.object(artifacts, "snapshot_download", return_value=str(root)):
25
  paths = artifacts.download_checkpoint("owner/model", "checkpoints/30000")
26
  self.assertEqual(paths.checkpoint, checkpoint)
27
+ self.assertEqual(paths.norm_stats, checkpoint / "assets/ur_demo/norm_stats.json")
28
+
29
+ def test_download_checkpoint_accepts_named_asset_directory(self):
30
+ import artifacts
31
+
32
+ with tempfile.TemporaryDirectory() as directory:
33
+ root = Path(directory)
34
+ checkpoint = root / "3000"
35
+ (checkpoint / "params/ocdbt.process_0/d").mkdir(parents=True)
36
+ stats = checkpoint / "assets/F-Fer/ur-1/norm_stats.json"
37
+ stats.parent.mkdir(parents=True)
38
+ stats.write_text("{}")
39
+ with mock.patch.object(artifacts, "snapshot_download", return_value=str(root)):
40
+ paths = artifacts.download_checkpoint("owner/model", "3000")
41
+ self.assertEqual(paths.norm_stats, stats)
42
 
43
  def test_download_checkpoint_reports_missing_weights(self):
44
  import artifacts