BrianZhengJourney commited on
Commit
8394222
·
verified ·
1 Parent(s): d269cc8

xctrl server: bread convention is 20D state in / 14D actions out (tofu template was 14D state)

Browse files
Files changed (1) hide show
  1. deploy/serve_bread_xctrl.py +46 -33
deploy/serve_bread_xctrl.py CHANGED
@@ -1,16 +1,21 @@
1
  """Serve the bread R1 policy (pi05_bread_r1_lora) over WebSocket, xctrl-style.
2
 
3
- Modeled on Tianming's poke/serve_xctrl_openpi_policy.py flat-obs contract.
 
 
 
4
 
5
  Client sends a flat obs dict:
6
  {
7
- "state": ndarray(14,), # per arm [xyz(3), axis_angle(3), grip(1)], L then R
8
- # ABSOLUTE grasp_site poses <-- CONFIRM layout/frame
 
 
9
  "camera_ego": ndarray(H, W, 3), # accepted but IGNORED (model's base slot is masked)
10
  "camera_left": ndarray(H, W, 3),
11
  "camera_right": ndarray(H, W, 3),
12
- "prompt": str, # optional; defaults to the training prompt
13
- "reset": bool, # optional; True on the first frame of an episode
14
  }
15
 
16
  Server returns:
@@ -21,19 +26,16 @@ Server returns:
21
  ... (openpi policy timing fields passed through)
22
  }
23
 
24
- Differences vs the cooking server -- all handled inside this shim:
25
- 1. key names: our BreadInputs takes left_wrist_image / right_wrist_image /
26
- state / prompt (no ego/base camera -- it was a human egocam in training).
27
- 2. state conversion: client 14D axis-angle -> model 20D rot6d.
28
- 3. relative_to_first: the model was trained on states re-expressed in each
29
- arm's EPISODE-START frame. This server is therefore STATEFUL: it records
30
- the first absolute pose after start/reset and re-expresses every
31
- subsequent state. Send {"reset": True} at every new episode, or restart
32
- the server. Getting this wrong produces confidently wrong actions.
33
 
34
  Usage:
35
  uv run python deploy/serve_bread_xctrl.py \
36
- --port=8000 \
37
  policy:checkpoint \
38
  --policy.config=pi05_bread_r1_lora \
39
  --policy.dir=/PATH/TO/pi05-bread-r1-top200/10000
@@ -44,11 +46,10 @@ from __future__ import annotations
44
  import dataclasses
45
  import logging
46
  import socket
47
- from typing import Any
48
 
49
  import numpy as np
50
  import tyro
51
- from scipy.spatial.transform import Rotation
52
 
53
  from openpi.policies import policy_config as _policy_config
54
  from openpi.serving import websocket_policy_server # swap for xctrl.policies.WebSocketPolicyServer inside xctrl
@@ -57,10 +58,17 @@ from openpi.training import config as _config
57
  TRAIN_PROMPT = "take the bread out of the bowl and put the bread into the toaster"
58
 
59
 
60
- def _pose_from_7d(x: np.ndarray) -> np.ndarray:
61
- """[xyz(3), axis_angle(3)] -> 4x4."""
 
 
 
 
 
 
 
62
  T = np.eye(4)
63
- T[:3, :3] = Rotation.from_rotvec(x[3:6]).as_matrix()
64
  T[:3, 3] = x[:3]
65
  return T
66
 
@@ -79,31 +87,35 @@ class Checkpoint:
79
  class Args:
80
  port: int = 8000
81
  default_prompt: str = TRAIN_PROMPT
 
82
  policy: Checkpoint = dataclasses.field(default_factory=Checkpoint)
83
 
84
 
85
  class BreadPolicyServer:
86
- """Flat xctrl obs -> BreadInputs keys, with stateful relative_to_first."""
87
 
88
- def __init__(self, openpi_policy, default_prompt: str = TRAIN_PROMPT) -> None:
 
89
  self._policy = openpi_policy
 
90
  self._default_prompt = default_prompt
91
- self._start_inv: list[np.ndarray] | None = None # [inv(T_L0), inv(T_R0)]
92
 
93
- def _state_20d(self, state14: np.ndarray, reset: bool) -> np.ndarray:
94
- state14 = np.asarray(state14, dtype=np.float64).reshape(14)
95
- poses = [_pose_from_7d(state14[0:6]), _pose_from_7d(state14[7:13])]
96
- grips = [state14[6], state14[13]]
 
97
  if reset or self._start_inv is None:
98
  self._start_inv = [np.linalg.inv(p) for p in poses]
99
  logging.info("relative_to_first anchor set (episode start)")
100
- out = np.zeros(20, dtype=np.float32)
101
- for i, (inv0, T, g) in enumerate(zip(self._start_inv, poses, grips)):
102
  rel = inv0 @ T
103
  off = i * 10
104
  out[off:off + 3] = rel[:3, 3]
105
  out[off + 3:off + 9] = _rot6d(rel[:3, :3])
106
- out[off + 9] = g
107
  return out
108
 
109
  def infer(self, obs: dict[str, Any]) -> dict[str, Any]:
@@ -111,7 +123,7 @@ class BreadPolicyServer:
111
  if isinstance(prompt, bytes):
112
  prompt = prompt.decode("utf-8")
113
  openpi_obs = {
114
- "state": self._state_20d(obs["state"], bool(obs.get("reset", False))),
115
  "left_wrist_image": np.asarray(obs["camera_left"]),
116
  "right_wrist_image": np.asarray(obs["camera_right"]),
117
  "prompt": str(prompt),
@@ -134,10 +146,11 @@ def main(args: Args) -> None:
134
  _policy_config.create_trained_policy(
135
  _config.get_config(args.policy.config), args.policy.dir,
136
  default_prompt=args.default_prompt),
 
137
  default_prompt=args.default_prompt)
138
  hostname = socket.gethostname()
139
- logging.info("bread policy server on %s (%s):%d", hostname,
140
- socket.gethostbyname(hostname), args.port)
141
  websocket_policy_server.WebsocketPolicyServer(
142
  policy=policy, host="0.0.0.0", port=args.port).serve_forever()
143
 
 
1
  """Serve the bread R1 policy (pi05_bread_r1_lora) over WebSocket, xctrl-style.
2
 
3
+ Modeled on Tianming's poke/serve_xctrl_openpi_policy.py -- BUT that template
4
+ is from the cooking (tofu) project whose state is 14D. The bread convention
5
+ agreed with Zhengmao is **20D state in / 14D actions out**, which matches
6
+ this model exactly, so the shim below is mostly key renaming.
7
 
8
  Client sends a flat obs dict:
9
  {
10
+ "state": ndarray(20,), # per arm [xyz(3), rot6d(6), grip(1)], L then R,
11
+ # grasp_site frame.
12
+ # DEFAULT ASSUMPTION: already relative_to_first
13
+ # (Zhengmao's yam_umi convention). <-- CONFIRM
14
  "camera_ego": ndarray(H, W, 3), # accepted but IGNORED (model's base slot is masked)
15
  "camera_left": ndarray(H, W, 3),
16
  "camera_right": ndarray(H, W, 3),
17
+ "prompt": str, # optional; defaults to the training prompt
18
+ "reset": bool, # optional; only used in --state-mode=absolute
19
  }
20
 
21
  Server returns:
 
26
  ... (openpi policy timing fields passed through)
27
  }
28
 
29
+ --state-mode:
30
+ relative (default) executor already re-expresses each arm's pose in its
31
+ episode-start frame -> passthrough.
32
+ absolute executor sends absolute poses -> this server records the first
33
+ pose after start/{"reset": True} and re-expresses (STATEFUL:
34
+ send reset at every new episode or actions will be garbage).
 
 
 
35
 
36
  Usage:
37
  uv run python deploy/serve_bread_xctrl.py \
38
+ --port=8000 --state-mode=relative \
39
  policy:checkpoint \
40
  --policy.config=pi05_bread_r1_lora \
41
  --policy.dir=/PATH/TO/pi05-bread-r1-top200/10000
 
46
  import dataclasses
47
  import logging
48
  import socket
49
+ from typing import Any, Literal
50
 
51
  import numpy as np
52
  import tyro
 
53
 
54
  from openpi.policies import policy_config as _policy_config
55
  from openpi.serving import websocket_policy_server # swap for xctrl.policies.WebSocketPolicyServer inside xctrl
 
58
  TRAIN_PROMPT = "take the bread out of the bowl and put the bread into the toaster"
59
 
60
 
61
+ def _rot6d_to_matrix(r6: np.ndarray) -> np.ndarray:
62
+ a, b = r6[:3], r6[3:]
63
+ x = a / np.linalg.norm(a)
64
+ b = b - np.dot(x, b) * x
65
+ y = b / np.linalg.norm(b)
66
+ return np.stack([x, y, np.cross(x, y)], axis=1)
67
+
68
+
69
+ def _pose_from_10d(x: np.ndarray) -> np.ndarray:
70
  T = np.eye(4)
71
+ T[:3, :3] = _rot6d_to_matrix(x[3:9])
72
  T[:3, 3] = x[:3]
73
  return T
74
 
 
87
  class Args:
88
  port: int = 8000
89
  default_prompt: str = TRAIN_PROMPT
90
+ state_mode: Literal["relative", "absolute"] = "relative"
91
  policy: Checkpoint = dataclasses.field(default_factory=Checkpoint)
92
 
93
 
94
  class BreadPolicyServer:
95
+ """Flat xctrl obs -> BreadInputs keys (20D state passthrough or re-expression)."""
96
 
97
+ def __init__(self, openpi_policy, state_mode: str = "relative",
98
+ default_prompt: str = TRAIN_PROMPT) -> None:
99
  self._policy = openpi_policy
100
+ self._state_mode = state_mode
101
  self._default_prompt = default_prompt
102
+ self._start_inv: list[np.ndarray] | None = None # absolute mode only
103
 
104
+ def _state(self, state20: np.ndarray, reset: bool) -> np.ndarray:
105
+ state20 = np.asarray(state20, dtype=np.float64).reshape(20)
106
+ if self._state_mode == "relative":
107
+ return state20.astype(np.float32)
108
+ poses = [_pose_from_10d(state20[0:10]), _pose_from_10d(state20[10:20])]
109
  if reset or self._start_inv is None:
110
  self._start_inv = [np.linalg.inv(p) for p in poses]
111
  logging.info("relative_to_first anchor set (episode start)")
112
+ out = state20.astype(np.float32).copy()
113
+ for i, (inv0, T) in enumerate(zip(self._start_inv, poses)):
114
  rel = inv0 @ T
115
  off = i * 10
116
  out[off:off + 3] = rel[:3, 3]
117
  out[off + 3:off + 9] = _rot6d(rel[:3, :3])
118
+ # gripper (off+9) passes through unchanged
119
  return out
120
 
121
  def infer(self, obs: dict[str, Any]) -> dict[str, Any]:
 
123
  if isinstance(prompt, bytes):
124
  prompt = prompt.decode("utf-8")
125
  openpi_obs = {
126
+ "state": self._state(obs["state"], bool(obs.get("reset", False))),
127
  "left_wrist_image": np.asarray(obs["camera_left"]),
128
  "right_wrist_image": np.asarray(obs["camera_right"]),
129
  "prompt": str(prompt),
 
146
  _policy_config.create_trained_policy(
147
  _config.get_config(args.policy.config), args.policy.dir,
148
  default_prompt=args.default_prompt),
149
+ state_mode=args.state_mode,
150
  default_prompt=args.default_prompt)
151
  hostname = socket.gethostname()
152
+ logging.info("bread policy server on %s (%s):%d state_mode=%s", hostname,
153
+ socket.gethostbyname(hostname), args.port, args.state_mode)
154
  websocket_policy_server.WebsocketPolicyServer(
155
  policy=policy, host="0.0.0.0", port=args.port).serve_forever()
156