MichaelMintIcecream commited on
Commit
df7bb3b
Β·
verified Β·
1 Parent(s): 00337e7

step 4: endpoint-ready server (molmoact2_server.py)

Browse files
Files changed (1) hide show
  1. molmoact2_server.py +66 -14
molmoact2_server.py CHANGED
@@ -25,6 +25,7 @@ import os
25
  import secrets
26
  import threading
27
  import time
 
28
  from typing import Optional
29
 
30
  import numpy as np
@@ -37,6 +38,11 @@ from transformers import AutoModelForImageTextToText, AutoProcessor
37
  import rtc as rtcmod
38
 
39
  REPO_ID = os.environ.get("MOLMOACT_REPO", "allenai/MolmoAct2-SO100_101")
 
 
 
 
 
40
  NORM_TAG = os.environ.get("MOLMOACT_NORM_TAG", "so100_so101_molmoact2")
41
  AUTH_TOKEN = os.environ.get("NORI_INFER_TOKEN") # REQUIRED β€” the rollout sends it
42
  # bf16 fits <16GB (A10G/L4). Set MOLMOACT_BF16=0 to run fp32 (~26GB, needs L40S/48GB).
@@ -59,6 +65,20 @@ _rtc_state = rtcmod.RTCState()
59
  _rtc_prev: dict = {} # session id -> previous chunk (normalized, on-device)
60
  _RTC_SESSION_CAP = 8 # bound the cache; robot sessions are few and long-lived
61
  _load_error: Optional[str] = None # set if the background load failed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
 
64
  def _load_model() -> None:
@@ -66,14 +86,19 @@ def _load_model() -> None:
66
 
67
  MolmoAct2 is ~21GB β€” a blocking startup event would keep the port dark for
68
  minutes and a HuggingFace Space health-probe would kill the container as
69
- unhealthy before the model ever finishes loading. /health reports progress.
 
70
  """
71
- global _model, _processor, _load_error
72
  try:
73
- proc = AutoProcessor.from_pretrained(REPO_ID, trust_remote_code=True)
 
 
 
 
74
  model = (
75
  AutoModelForImageTextToText.from_pretrained(
76
- REPO_ID, trust_remote_code=True, dtype=DTYPE
77
  )
78
  .to("cuda")
79
  .eval()
@@ -86,7 +111,7 @@ def _load_model() -> None:
86
  except Exception as exc:
87
  print(f"[molmoact2] RTC install failed ({exc}) β€” serving un-guided", flush=True)
88
  _processor, _model = proc, model
89
- print(f"[molmoact2] loaded {REPO_ID} dtype={DTYPE} (RTC patch installed)", flush=True)
90
  except Exception as exc: # surface load failures via /health instead of a dead port
91
  _load_error = f"{type(exc).__name__}: {exc}"
92
  print(f"[molmoact2] LOAD FAILED β€” {_load_error}", flush=True)
@@ -99,6 +124,21 @@ def _startup() -> None:
99
  threading.Thread(target=_load_model, name="molmoact2-load", daemon=True).start()
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  class ActRequest(BaseModel):
103
  images: list[str] # base64 JPEG/PNG (optionally a data: URL), 2+ camera views
104
  state: list[float] # robot joint state (6 for a single SO-100/101 arm)
@@ -147,7 +187,7 @@ def _decode(b64: str) -> np.ndarray:
147
  def _status() -> dict:
148
  status = "ready" if _model is not None else ("error" if _load_error else "loading")
149
  return {"ok": _model is not None, "status": status, "error": _load_error,
150
- "repo": REPO_ID, "dtype": str(DTYPE)}
151
 
152
 
153
  @app.get("/")
@@ -164,6 +204,20 @@ def health() -> dict:
164
  return _status()
165
 
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  class PointRequest(BaseModel):
168
  image: str # base64 JPEG/PNG, one camera view
169
  query: str = "the red cup"
@@ -177,15 +231,15 @@ class PointResponse(BaseModel):
177
 
178
 
179
  @app.post("/point", response_model=PointResponse)
180
- def point(req: PointRequest, authorization: Optional[str] = Header(None)) -> PointResponse:
 
181
  """Perception probe (diagnostic, not on the control path): ask the Molmo2-ER
182
  backbone β€” a pixel-accurate pointing model β€” to point at `query` in ONE
183
  frame. Separates "does the model SEE the target in our camera domain" from
184
  "does it act correctly": wrong/absent points on live robot frames = visual
185
  domain gap (no calibration work can fix it); correct points + wrong motion
186
  = the failure is downstream of perception."""
187
- if not authorization or not secrets.compare_digest(authorization, f"Bearer {AUTH_TOKEN}"):
188
- raise HTTPException(status_code=401, detail="bad or missing bearer token")
189
  if _model is None:
190
  detail = f"model load failed: {_load_error}" if _load_error else "model not loaded yet"
191
  raise HTTPException(status_code=503, detail=detail)
@@ -225,11 +279,9 @@ def point(req: PointRequest, authorization: Optional[str] = Header(None)) -> Poi
225
 
226
 
227
  @app.post("/act", response_model=ActResponse)
228
- def act(req: ActRequest, authorization: Optional[str] = Header(None)) -> ActResponse:
229
- # Constant-time compare so a bad token can't be recovered via response timing.
230
- # Checked BEFORE any model work so unauthenticated calls never touch the GPU.
231
- if not authorization or not secrets.compare_digest(authorization, f"Bearer {AUTH_TOKEN}"):
232
- raise HTTPException(status_code=401, detail="bad or missing bearer token")
233
  if _model is None:
234
  detail = f"model load failed: {_load_error}" if _load_error else "model not loaded yet"
235
  raise HTTPException(status_code=503, detail=detail)
 
25
  import secrets
26
  import threading
27
  import time
28
+ from pathlib import Path
29
  from typing import Optional
30
 
31
  import numpy as np
 
38
  import rtc as rtcmod
39
 
40
  REPO_ID = os.environ.get("MOLMOACT_REPO", "allenai/MolmoAct2-SO100_101")
41
+ # Inference Endpoints mount the endpoint's model repo at /repository (platform
42
+ # fast-path β€” no 21GB Hub download at boot). Load from there when present, else
43
+ # fall back to the Hub download so the SAME image still runs as a Docker Space
44
+ # during the transition. Override the probe location with MODEL_PATH.
45
+ MODEL_PATH = os.environ.get("MODEL_PATH", "/repository")
46
  NORM_TAG = os.environ.get("MOLMOACT_NORM_TAG", "so100_so101_molmoact2")
47
  AUTH_TOKEN = os.environ.get("NORI_INFER_TOKEN") # REQUIRED β€” the rollout sends it
48
  # bf16 fits <16GB (A10G/L4). Set MOLMOACT_BF16=0 to run fp32 (~26GB, needs L40S/48GB).
 
65
  _rtc_prev: dict = {} # session id -> previous chunk (normalized, on-device)
66
  _RTC_SESSION_CAP = 8 # bound the cache; robot sessions are few and long-lived
67
  _load_error: Optional[str] = None # set if the background load failed
68
+ _model_source: Optional[str] = None # /repository mount or the Hub repo id
69
+
70
+
71
+ def _resolve_model_source() -> str:
72
+ """Prefer the platform-mounted weights (Inference Endpoints: /repository);
73
+ fall back to the Hub repo id (Docker Space / bare GPU box). A non-empty dir
74
+ is treated as the mount β€” trust_remote_code loads the model code from it."""
75
+ p = Path(MODEL_PATH)
76
+ try:
77
+ if p.is_dir() and any(p.iterdir()):
78
+ return str(p)
79
+ except OSError:
80
+ pass
81
+ return REPO_ID
82
 
83
 
84
  def _load_model() -> None:
 
86
 
87
  MolmoAct2 is ~21GB β€” a blocking startup event would keep the port dark for
88
  minutes and a HuggingFace Space health-probe would kill the container as
89
+ unhealthy before the model ever finishes loading. /health reports progress;
90
+ /ready gives probes the 503-until-loaded semantic (Endpoints health_route).
91
  """
92
+ global _model, _processor, _load_error, _model_source
93
  try:
94
+ _model_source = _resolve_model_source()
95
+ print(f"[molmoact2] loading from {_model_source} "
96
+ f"({'mounted /repository' if _model_source != REPO_ID else 'Hub download'})",
97
+ flush=True)
98
+ proc = AutoProcessor.from_pretrained(_model_source, trust_remote_code=True)
99
  model = (
100
  AutoModelForImageTextToText.from_pretrained(
101
+ _model_source, trust_remote_code=True, dtype=DTYPE
102
  )
103
  .to("cuda")
104
  .eval()
 
111
  except Exception as exc:
112
  print(f"[molmoact2] RTC install failed ({exc}) β€” serving un-guided", flush=True)
113
  _processor, _model = proc, model
114
+ print(f"[molmoact2] loaded {_model_source} dtype={DTYPE} (RTC patch installed)", flush=True)
115
  except Exception as exc: # surface load failures via /health instead of a dead port
116
  _load_error = f"{type(exc).__name__}: {exc}"
117
  print(f"[molmoact2] LOAD FAILED β€” {_load_error}", flush=True)
 
124
  threading.Thread(target=_load_model, name="molmoact2-load", daemon=True).start()
125
 
126
 
127
+ def _require_auth(x_nori_token: Optional[str], authorization: Optional[str]) -> None:
128
+ """App-level auth for /act and /point. `X-Nori-Token` is the PRIMARY
129
+ credential: on a *protected* Inference Endpoint HF's edge consumes the
130
+ `Authorization` header (it must carry an HF token to get past the proxy), so
131
+ our own bearer can no longer ride it β€” custom headers pass through untouched.
132
+ `Authorization: Bearer <token>` stays accepted for the Space-transition
133
+ client (which sends BOTH). Each comparison is constant-time; checked BEFORE
134
+ any model work so unauthenticated calls never touch the GPU."""
135
+ if x_nori_token and secrets.compare_digest(x_nori_token, AUTH_TOKEN):
136
+ return
137
+ if authorization and secrets.compare_digest(authorization, f"Bearer {AUTH_TOKEN}"):
138
+ return
139
+ raise HTTPException(status_code=401, detail="bad or missing auth token")
140
+
141
+
142
  class ActRequest(BaseModel):
143
  images: list[str] # base64 JPEG/PNG (optionally a data: URL), 2+ camera views
144
  state: list[float] # robot joint state (6 for a single SO-100/101 arm)
 
187
  def _status() -> dict:
188
  status = "ready" if _model is not None else ("error" if _load_error else "loading")
189
  return {"ok": _model is not None, "status": status, "error": _load_error,
190
+ "repo": REPO_ID, "source": _model_source, "dtype": str(DTYPE)}
191
 
192
 
193
  @app.get("/")
 
204
  return _status()
205
 
206
 
207
+ @app.get("/ready")
208
+ def ready() -> dict:
209
+ """Readiness with 503-until-loaded semantics β€” set this as the Inference
210
+ Endpoint's `health_route` so the platform routes no traffic (and marks the
211
+ replica initializing) until the model is actually servable. Kept SEPARATE
212
+ from `/` and `/health`, which must stay 200-while-loading: a Docker Space
213
+ routes external traffic only after a 2xx on `/`, so a 503 there would keep
214
+ the Space dark for the whole model load."""
215
+ if _model is None:
216
+ detail = f"model load failed: {_load_error}" if _load_error else "model loading"
217
+ raise HTTPException(status_code=503, detail=detail)
218
+ return _status()
219
+
220
+
221
  class PointRequest(BaseModel):
222
  image: str # base64 JPEG/PNG, one camera view
223
  query: str = "the red cup"
 
231
 
232
 
233
  @app.post("/point", response_model=PointResponse)
234
+ def point(req: PointRequest, authorization: Optional[str] = Header(None),
235
+ x_nori_token: Optional[str] = Header(None)) -> PointResponse:
236
  """Perception probe (diagnostic, not on the control path): ask the Molmo2-ER
237
  backbone β€” a pixel-accurate pointing model β€” to point at `query` in ONE
238
  frame. Separates "does the model SEE the target in our camera domain" from
239
  "does it act correctly": wrong/absent points on live robot frames = visual
240
  domain gap (no calibration work can fix it); correct points + wrong motion
241
  = the failure is downstream of perception."""
242
+ _require_auth(x_nori_token, authorization)
 
243
  if _model is None:
244
  detail = f"model load failed: {_load_error}" if _load_error else "model not loaded yet"
245
  raise HTTPException(status_code=503, detail=detail)
 
279
 
280
 
281
  @app.post("/act", response_model=ActResponse)
282
+ def act(req: ActRequest, authorization: Optional[str] = Header(None),
283
+ x_nori_token: Optional[str] = Header(None)) -> ActResponse:
284
+ _require_auth(x_nori_token, authorization)
 
 
285
  if _model is None:
286
  detail = f"model load failed: {_load_error}" if _load_error else "model not loaded yet"
287
  raise HTTPException(status_code=503, detail=detail)