MichaelMintIcecream commited on
Commit
40fa6ec
·
verified ·
1 Parent(s): b5294ae

spike: MolmoAct2 Docker Space (server + Dockerfile)

Browse files
DEPLOY.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploy MolmoAct2 as a HuggingFace Docker Space
2
+
3
+ This folder IS the Space contents (`Dockerfile`, `README.md`, `requirements.txt`,
4
+ `molmoact2_server.py`). `molmoact2_server.py` is a copy of the canonical
5
+ `../molmoact2_server.py` — after editing the canonical server, re-copy it:
6
+
7
+ ```bash
8
+ cp ../molmoact2_server.py molmoact2_server.py
9
+ ```
10
+
11
+ ## Target
12
+ Space `NoriRobotics/molmoact2-space` (private, Docker SDK, GPU hardware).
13
+
14
+ ## Path A — scripted (create repo + upload folder)
15
+ Uses the org token already in `nori-backend/.env` (`HF_ORG_ADMIN_TOKEN`), read
16
+ in-process, never printed. From `cloud_inference/space/`:
17
+
18
+ ```bash
19
+ python - <<'PY'
20
+ import os
21
+ from dotenv import load_dotenv
22
+ from huggingface_hub import HfApi
23
+ load_dotenv("/Users/michael/Documents/Nori-Robotics/nori-backend/.env")
24
+ api = HfApi(token=os.environ["HF_ORG_ADMIN_TOKEN"])
25
+ repo = "NoriRobotics/molmoact2-space"
26
+ api.create_repo(repo, repo_type="space", space_sdk="docker", private=True, exist_ok=True)
27
+ api.upload_folder(repo_id=repo, repo_type="space", folder_path=".")
28
+ print("uploaded ->", repo)
29
+ PY
30
+ ```
31
+
32
+ Creating the Space defaults to **free CPU** hardware; a CPU box cannot load the
33
+ model. After upload, finish in the UI (Path B steps 2-3).
34
+
35
+ ## Path B — UI
36
+ 1. huggingface.co → New Space → owner `NoriRobotics`, SDK **Docker**, **private**.
37
+ Clone it, copy this folder's files in, `git add . && git commit && git push`.
38
+ 2. **Settings → Variables and secrets**: add secret `NORI_INFER_TOKEN` (the bearer
39
+ token the rollout sends). Add `HF_TOKEN` only if the model repo is gated.
40
+ 3. **Settings → Hardware**: pick a GPU — `a10g-small` (~$1/hr) is enough.
41
+
42
+ ## Verify
43
+ First boot downloads ~21GB. Poll health (no auth needed):
44
+
45
+ ```bash
46
+ curl -s https://noribotics-molmoact2-space.hf.space/health
47
+ # {"ok":false,"status":"loading",...} -> {"ok":true,"status":"ready",...}
48
+ ```
49
+
50
+ Smoke-test `/act` (needs the token; keep it out of shell history — read from a file):
51
+
52
+ ```bash
53
+ TOKEN=$(cat ~/.nori_infer_token) # or your secret manager
54
+ curl -s https://noribotics-molmoact2-space.hf.space/act \
55
+ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
56
+ -d '{"images":["<b64-jpeg>","<b64-jpeg>"],"state":[0,0,0,0,0,0],
57
+ "instruction":"pick up the red cup","num_steps":10}'
58
+ # -> {"actions":[[...6...], ...]} ~30 moves, robot scale
59
+ ```
60
+
61
+ The exact subdomain is shown on the Space page (Embed → Direct URL). It is
62
+ `https://<owner>-<space-name>.hf.space`, lowercased with `/` → `-`.
63
+
64
+ ## Cost note
65
+ A GPU Space bills while **Running**. Set **Sleep after inactivity** in Settings
66
+ (e.g. 15 min) for the spike so it pauses when idle; the robot rollout wakes it
67
+ (cold start ≈ first-boot download unless persistent storage is attached).
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MolmoAct2-SO100_101 cloud-inference server as a HuggingFace **Docker** Space.
2
+ #
3
+ # WHY A DOCKER SPACE (not an Inference Endpoint): the managed Inference-Endpoint
4
+ # container bakes in `huggingface_inference_toolkit`, whose bootstrap imports
5
+ # `transformers.file_utils.is_tf_available` — REMOVED in the transformers >=4.57
6
+ # that MolmoAct2's processor requires (it needs `transformers.video_utils`). The
7
+ # two are mutually exclusive, so the toolkit path is a dead end. A Docker Space
8
+ # runs OUR uvicorn directly — the toolkit never enters the picture, and we own
9
+ # the exact transformers version.
10
+ #
11
+ # Base image = the SAME torch stack proven on HF Jobs (torch 2.5.1 + cu121):
12
+ # pinning transformers on top of it does NOT upgrade torch, so torchvision /
13
+ # torchaudio ABIs stay intact (the "-U torchvision" ABI break we hit earlier).
14
+ FROM pytorch/pytorch:2.5.1-cuda12.1-cudnn9-runtime
15
+
16
+ # HF Spaces run the container as uid 1000 with $HOME=/home/user. Point every
17
+ # cache at a writable dir so the ~21GB model download doesn't hit a read-only FS.
18
+ ENV HOME=/home/user \
19
+ PYTHONUNBUFFERED=1 \
20
+ HF_HOME=/home/user/.cache/huggingface \
21
+ HF_HUB_ENABLE_HF_TRANSFER=1
22
+
23
+ # libGL / glib for PIL+torchvision image ops; ffmpeg libs for PyAV (av) decode.
24
+ RUN apt-get update && apt-get install -y --no-install-recommends \
25
+ ffmpeg libgl1 libglib2.0-0 && \
26
+ rm -rf /var/lib/apt/lists/*
27
+
28
+ RUN useradd -m -u 1000 user
29
+
30
+ WORKDIR /app
31
+ COPY --chown=user:user requirements.txt .
32
+ RUN pip install --no-cache-dir -r requirements.txt hf_transfer
33
+
34
+ COPY --chown=user:user molmoact2_server.py .
35
+
36
+ RUN mkdir -p /home/user/.cache/huggingface && chown -R user:user /home/user /app
37
+ USER user
38
+
39
+ # HF Spaces expose the container on 7860 (declared as app_port in README.md).
40
+ EXPOSE 7860
41
+ CMD ["uvicorn", "molmoact2_server:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,33 @@
1
  ---
2
- title: Molmoact2 Space
3
- emoji: 📚
4
- colorFrom: blue
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MolmoAct2 SO100 101 Inference
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ short_description: Nori cloud-inference server for MolmoAct2-SO100_101 (/act)
10
  ---
11
 
12
+ # MolmoAct2-SO100_101 Nori cloud inference
13
+
14
+ Private Docker Space serving `allenai/MolmoAct2-SO100_101` for Nori robot rollout.
15
+ It runs our own FastAPI/uvicorn server (`molmoact2_server.py`) — **not** the HF
16
+ Inference-Endpoint toolkit, which is incompatible with the transformers version
17
+ this model needs.
18
+
19
+ ## Endpoints
20
+ - `GET /health` → `{"ok", "status": "loading|ready|error", "error", "repo", "dtype"}`
21
+ - `POST /act` (Bearer `NORI_INFER_TOKEN`) →
22
+ `{ images:[b64...], state:[6 floats], instruction:str, num_steps? }`
23
+ → `{ actions: [[...6 DOF...], ... up to 30 moves] }` (robot scale).
24
+
25
+ ## Required setup (Space **Settings**)
26
+ 1. **Hardware**: a GPU tier — `a10g-small` (A10G 24GB, ~$1/hr) is enough (bf16 <16GB).
27
+ 2. **Secrets**:
28
+ - `NORI_INFER_TOKEN` — the bearer token the rollout client sends (required).
29
+ - `HF_TOKEN` — only if the model repo is gated (allenai's is public; usually not needed).
30
+ 3. First boot downloads ~21GB, so `/health` reports `"loading"` for a few minutes,
31
+ then `"ready"`. Add **persistent storage** later to skip re-downloads on restart.
32
+
33
+ Deploy/update instructions: see `cloud_inference/space/DEPLOY.md` in the Nori-Lab repo.
__pycache__/molmoact2_server.cpython-312.pyc ADDED
Binary file (7.91 kB). View file
 
molmoact2_server.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nori cloud-inference server for MolmoAct2-SO100_101 (spike — task #38).
3
+
4
+ Runs on an AWS GPU instance (g5.xlarge / A10G 24GB is enough in bf16 <16GB).
5
+ Serves the robot rollout over plain JSON (NO pickle on the wire — avoids the
6
+ LeRobot PolicyServer CVE-2026-25874 class):
7
+
8
+ POST /act { images:[b64...], state:[6 floats], instruction:str, num_steps? }
9
+ -> { actions: [[...DOF...], ...] } # a 10-30 move chunk, ROBOT SCALE
10
+
11
+ The model is loaded once at startup. Inference is serialized behind a lock
12
+ (single GPU). Bearer-token auth (NORI_INFER_TOKEN) on every call.
13
+
14
+ The exact model API mirrors the allenai/MolmoAct2-SO100_101 model card:
15
+ model.predict_action(processor=..., images=[...], task=..., state=...,
16
+ norm_tag="so100_so101_molmoact2", inference_action_mode="continuous",
17
+ num_steps=10, normalize_language=True, enable_cuda_graph=True).actions
18
+
19
+ Deploy + test: see README.md in this directory.
20
+ """
21
+
22
+ import base64
23
+ import io
24
+ import os
25
+ import threading
26
+ from typing import Optional
27
+
28
+ import numpy as np
29
+ import torch
30
+ from fastapi import FastAPI, Header, HTTPException
31
+ from PIL import Image
32
+ from pydantic import BaseModel
33
+ from transformers import AutoModelForImageTextToText, AutoProcessor
34
+
35
+ REPO_ID = os.environ.get("MOLMOACT_REPO", "allenai/MolmoAct2-SO100_101")
36
+ NORM_TAG = os.environ.get("MOLMOACT_NORM_TAG", "so100_so101_molmoact2")
37
+ AUTH_TOKEN = os.environ.get("NORI_INFER_TOKEN") # REQUIRED — the rollout sends it
38
+ # bf16 fits <16GB (A10G/L4). Set MOLMOACT_BF16=0 to run fp32 (~26GB, needs L40S/48GB).
39
+ DTYPE = torch.bfloat16 if os.environ.get("MOLMOACT_BF16", "1") == "1" else torch.float32
40
+
41
+ app = FastAPI(title="nori-molmoact2")
42
+ _model = None
43
+ _processor = None
44
+ _lock = threading.Lock() # single GPU: serialize predict_action calls
45
+ _load_error: Optional[str] = None # set if the background load failed
46
+
47
+
48
+ def _load_model() -> None:
49
+ """Load weights in a background thread so the HTTP port is up immediately.
50
+
51
+ MolmoAct2 is ~21GB — a blocking startup event would keep the port dark for
52
+ minutes and a HuggingFace Space health-probe would kill the container as
53
+ unhealthy before the model ever finishes loading. /health reports progress.
54
+ """
55
+ global _model, _processor, _load_error
56
+ try:
57
+ proc = AutoProcessor.from_pretrained(REPO_ID, trust_remote_code=True)
58
+ model = (
59
+ AutoModelForImageTextToText.from_pretrained(
60
+ REPO_ID, trust_remote_code=True, dtype=DTYPE
61
+ )
62
+ .to("cuda")
63
+ .eval()
64
+ )
65
+ _processor, _model = proc, model
66
+ print(f"[molmoact2] loaded {REPO_ID} dtype={DTYPE}", flush=True)
67
+ except Exception as exc: # surface load failures via /health instead of a dead port
68
+ _load_error = f"{type(exc).__name__}: {exc}"
69
+ print(f"[molmoact2] LOAD FAILED — {_load_error}", flush=True)
70
+
71
+
72
+ @app.on_event("startup")
73
+ def _startup() -> None:
74
+ if not AUTH_TOKEN:
75
+ raise RuntimeError("NORI_INFER_TOKEN must be set (bearer token for /act)")
76
+ threading.Thread(target=_load_model, name="molmoact2-load", daemon=True).start()
77
+
78
+
79
+ class ActRequest(BaseModel):
80
+ images: list[str] # base64 JPEG/PNG (optionally a data: URL), 2+ camera views
81
+ state: list[float] # robot joint state (6 for a single SO-100/101 arm)
82
+ instruction: str # natural-language task, e.g. "pick up the red cup"
83
+ num_steps: int = 10 # flow-matching integration steps (latency <-> quality)
84
+
85
+
86
+ class ActResponse(BaseModel):
87
+ actions: list[list[float]] # chunk: N moves x DOF, ROBOT SCALE (already de-normalized)
88
+
89
+
90
+ def _decode(b64: str) -> np.ndarray:
91
+ if b64.lstrip().startswith("data:") and "," in b64[:64]:
92
+ b64 = b64.split(",", 1)[1]
93
+ img = Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
94
+ return np.asarray(img)
95
+
96
+
97
+ @app.get("/health")
98
+ def health() -> dict:
99
+ status = "ready" if _model is not None else ("error" if _load_error else "loading")
100
+ return {"ok": _model is not None, "status": status, "error": _load_error,
101
+ "repo": REPO_ID, "dtype": str(DTYPE)}
102
+
103
+
104
+ @app.post("/act", response_model=ActResponse)
105
+ def act(req: ActRequest, authorization: Optional[str] = Header(None)) -> ActResponse:
106
+ if authorization != f"Bearer {AUTH_TOKEN}":
107
+ raise HTTPException(status_code=401, detail="bad or missing bearer token")
108
+ if _model is None:
109
+ detail = f"model load failed: {_load_error}" if _load_error else "model not loaded yet"
110
+ raise HTTPException(status_code=503, detail=detail)
111
+ if len(req.images) < 1:
112
+ raise HTTPException(status_code=422, detail="need at least one camera image")
113
+ images = [_decode(b) for b in req.images]
114
+ state = np.asarray(req.state, dtype=np.float32)
115
+ with _lock, torch.no_grad():
116
+ out = _model.predict_action(
117
+ processor=_processor,
118
+ images=images,
119
+ task=req.instruction,
120
+ state=state,
121
+ norm_tag=NORM_TAG,
122
+ inference_action_mode="continuous",
123
+ num_steps=req.num_steps,
124
+ normalize_language=True,
125
+ enable_cuda_graph=True,
126
+ )
127
+ acts = out.actions
128
+ if torch.is_tensor(acts): # predict_action returns a CUDA tensor — move to host first
129
+ acts = acts.detach().float().cpu().numpy()
130
+ acts = np.asarray(acts, dtype=np.float32)
131
+ if acts.ndim == 3 and acts.shape[0] == 1: # (1, chunk, DOF) -> (chunk, DOF)
132
+ acts = acts[0]
133
+ return ActResponse(actions=acts.tolist())
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Installed ON TOP OF the pytorch/pytorch:2.5.1-cuda12.1 base image.
2
+ #
3
+ # Do NOT list torch / torchvision / torchaudio here: the base image already
4
+ # ships a matched, ABI-compatible trio. Reinstalling/upgrading any one of them
5
+ # re-triggers the "libtorchaudio.so: undefined symbol" ABI break we hit on HF Jobs.
6
+ #
7
+ # transformers>=4.57.0 is MANDATORY — MolmoAct2's trust_remote_code processor
8
+ # imports transformers.video_utils.VideoInput (added in 4.57.0). Pinning it here
9
+ # is SAFE in a Docker Space (unlike the Inference-Endpoint toolkit, nothing else
10
+ # in this image imports the removed transformers.file_utils.is_tf_available).
11
+ transformers>=4.57.0
12
+ accelerate
13
+ fastapi
14
+ uvicorn[standard]
15
+ pillow
16
+ numpy
17
+ einops
18
+ av
19
+ scipy
20
+ requests