Add usage example, interleaved input spec, and download guidance
Browse files
README.md
CHANGED
|
@@ -17,6 +17,8 @@ datasets:
|
|
| 17 |
|
| 18 |
PRIMO R1 (Process Reasoning Induced Monitoring) is a 7B video multimodal large language model (MLLM) framework designed for accurate process supervision in long-horizon robotic manipulation. It was introduced in the paper [From Passive Observer to Active Critic: Reinforcement Learning Elicits Process Reasoning for Robotic Manipulation](https://huggingface.co/papers/2603.15600).
|
| 19 |
|
|
|
|
|
|
|
| 20 |
## Model Description
|
| 21 |
|
| 22 |
Current video MLLMs often function as passive "Observers" that recognize ongoing events rather than evaluating the current state relative to the final task goal. PRIMO R1 transforms these models into active "Critics" by:
|
|
@@ -25,12 +27,264 @@ Current video MLLMs often function as passive "Observers" that recognize ongoing
|
|
| 25 |
- **Temporal Anchoring**: Constructing a structured temporal input that explicitly anchors the video sequence between initial and current state images.
|
| 26 |
- **Process Reasoning**: Focusing on evaluating the current state against the intended task goal to detect failures and track progress.
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
## Performance
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
## Citations
|
| 36 |
|
|
|
|
| 17 |
|
| 18 |
PRIMO R1 (Process Reasoning Induced Monitoring) is a 7B video multimodal large language model (MLLM) framework designed for accurate process supervision in long-horizon robotic manipulation. It was introduced in the paper [From Passive Observer to Active Critic: Reinforcement Learning Elicits Process Reasoning for Robotic Manipulation](https://huggingface.co/papers/2603.15600).
|
| 19 |
|
| 20 |
+
This is the **final checkpoint**: a Qwen2.5-VL-7B-Instruct base taken through an SFT cold start and then GRPO reinforcement learning. Use this model unless you specifically want the ablation.
|
| 21 |
+
|
| 22 |
## Model Description
|
| 23 |
|
| 24 |
Current video MLLMs often function as passive "Observers" that recognize ongoing events rather than evaluating the current state relative to the final task goal. PRIMO R1 transforms these models into active "Critics" by:
|
|
|
|
| 27 |
- **Temporal Anchoring**: Constructing a structured temporal input that explicitly anchors the video sequence between initial and current state images.
|
| 28 |
- **Process Reasoning**: Focusing on evaluating the current state against the intended task goal to detect failures and track progress.
|
| 29 |
|
| 30 |
+
## Resources
|
| 31 |
+
|
| 32 |
+
| | |
|
| 33 |
+
| --- | --- |
|
| 34 |
+
| Code | [10-OASIS-01/PRIMO-R1](https://github.com/10-OASIS-01/PRIMO-R1) |
|
| 35 |
+
| Collection | [PRIMO R1](https://huggingface.co/collections/LeonOverload/primo-r1) |
|
| 36 |
+
| Paper | [arXiv 2603.15600](https://arxiv.org/abs/2603.15600) · [project page](https://10-oasis-01.github.io/primo-r1-website/) |
|
| 37 |
+
| Stage-1 ablation | [PRIMO-COT-SFT-7B](https://huggingface.co/LeonOverload/PRIMO-COT-SFT-7B) |
|
| 38 |
+
| Benchmark | [primo-bench-json](https://huggingface.co/datasets/LeonOverload/primo-bench-json) |
|
| 39 |
+
| Training data | [primo-sft-json](https://huggingface.co/datasets/LeonOverload/primo-sft-json) · [primo-rl-json](https://huggingface.co/datasets/LeonOverload/primo-rl-json) |
|
| 40 |
+
| Videos | [primo-video-media](https://huggingface.co/datasets/LeonOverload/primo-video-media) |
|
| 41 |
+
|
| 42 |
+
## Download
|
| 43 |
+
|
| 44 |
+
The `main` branch holds inference files only (**16.6 GB**). DeepSpeed resume state from RL step 2500 lives on a separate branch, so you do not pay ~100 GB for it by accident.
|
| 45 |
+
|
| 46 |
+
```python
|
| 47 |
+
from huggingface_hub import snapshot_download
|
| 48 |
+
|
| 49 |
+
# Inference: 16.6 GB
|
| 50 |
+
snapshot_download("LeonOverload/PRIMO-R1-7B", local_dir="models/PRIMO-R1-7B")
|
| 51 |
+
|
| 52 |
+
# Resume RL training from step 2500: ~116 GB
|
| 53 |
+
snapshot_download("LeonOverload/PRIMO-R1-7B", revision="training-state")
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
Or from the CLI:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
hf download LeonOverload/PRIMO-R1-7B --local-dir models/PRIMO-R1-7B
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
If you are on a revision where the optimizer state is still on `main`, exclude it explicitly. Note that `--exclude` is ignored if you also pass filenames positionally:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
hf download LeonOverload/PRIMO-R1-7B \
|
| 66 |
+
--exclude "global_step*" --exclude "rng_state*" \
|
| 67 |
+
--exclude "scheduler.pt" --exclude "latest" \
|
| 68 |
+
--local-dir models/PRIMO-R1-7B
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Setup
|
| 72 |
+
|
| 73 |
+
The environment matters more than usual here. Qwen2.5-VL support shifted between transformers releases, and installing a PyPI `transformers` over the pinned tree is the most common cause of shape and processor errors:
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
git clone https://github.com/10-OASIS-01/PRIMO-R1 && cd PRIMO-R1
|
| 77 |
+
conda create -n primo-r1 python=3.11 && conda activate primo-r1
|
| 78 |
+
bash setup.sh
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
`setup.sh` pins `vllm==0.7.2`, `trl==0.16.0`, and installs the vendored `transformers-main/` tree last so nothing replaces it.
|
| 82 |
+
|
| 83 |
+
## Input format — read this before running anything
|
| 84 |
+
|
| 85 |
+
PRIMO R1 is trained on an **interleaved** input that anchors the clip between two still frames. The content list must be in exactly this order:
|
| 86 |
+
|
| 87 |
+
```
|
| 88 |
+
image (initial frame) → video (the clip) → image (current frame) → text (question)
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Feeding a bare video clip will still produce output, but it degrades quality silently — the paper's Table 4 ablation measures this directly, and a current-state image alone raises average MAE from 15.52 to 59.50. Keep the order and keep both anchor frames.
|
| 92 |
+
|
| 93 |
+
The prompt template is equally load-bearing: the answer extractor is a regex over `<answer>...</answer>`, and the `<think>` block is required to contain `<planning>`, `<observation>`, and `<reasoning>` subsections in that order. Changing the template without changing the extractor collapses scores rather than raising an error.
|
| 94 |
+
|
| 95 |
+
## Usage
|
| 96 |
+
|
| 97 |
+
This example is transcribed from `src/eval/eval_interleave.py`, the harness that produced the published numbers.
|
| 98 |
+
|
| 99 |
+
```python
|
| 100 |
+
import cv2
|
| 101 |
+
import torch
|
| 102 |
+
from PIL import Image
|
| 103 |
+
from transformers import AutoProcessor, AutoTokenizer
|
| 104 |
+
from vllm import LLM, SamplingParams
|
| 105 |
+
from qwen_vl_utils import process_vision_info
|
| 106 |
+
|
| 107 |
+
MODEL_PATH = "models/PRIMO-R1-7B" # or "LeonOverload/PRIMO-R1-7B"
|
| 108 |
+
video_path = "path/to/your/episode.mp4"
|
| 109 |
+
question = "What is the completion percentage of the task in the video?"
|
| 110 |
+
problem_type = "regression"
|
| 111 |
+
|
| 112 |
+
SYSTEM_PROMPT = (
|
| 113 |
+
"A conversation between User and Assistant. The Assistant is an expert AI specializing in embodied procedure and event reasoning based on visual input (video or images). "
|
| 114 |
+
"The assistant must strictly follow a specific thought process and output format. "
|
| 115 |
+
"The reasoning process is enclosed within <think> </think> tags, and the final answer is within <answer> </answer> tags. "
|
| 116 |
+
"The <think> block must contain three ordered subsections: <planning>, <observation>, and <reasoning>. "
|
| 117 |
+
"The <answer> block must contain only the final output required by the question type and no other commentary."
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
QUESTION_TEMPLATE = (
|
| 121 |
+
"QUESTION:\n{Question}\n\n"
|
| 122 |
+
"QUESTION TYPE:\n{question_type}\n\n"
|
| 123 |
+
"Analyze the provided visual data and reason about the ongoing task.\n\n"
|
| 124 |
+
"Please think about this question as if you were a human pondering deeply. "
|
| 125 |
+
"Provide your detailed reasoning between the <think> and </think> tags, following the subsections <planning>, <observation>, and <reasoning>. "
|
| 126 |
+
"Then give your final answer between the <answer> and </answer> tags.\n\n"
|
| 127 |
+
"Below is the required template:\n\n"
|
| 128 |
+
"<think>\n"
|
| 129 |
+
"<planning>\n"
|
| 130 |
+
"Identify the high-level goal of the agent, what is the initial state? What does successful completion look like?\n"
|
| 131 |
+
"Break down the high-level goal into a logical sequence of canonical steps. This serves as your mental plan for interpreting the task.\n"
|
| 132 |
+
"Use this plan to interpret actions, map observed behaviors to steps, assess progress, detect anomalies, and predict what happens next.\n"
|
| 133 |
+
"</planning>\n"
|
| 134 |
+
"<observation>\n"
|
| 135 |
+
"View the video as a temporal sequence of actions contributing to the procedure.\n"
|
| 136 |
+
"Objectively describe what is occurring in the current moment, noting evidence of progress or state changes.\n"
|
| 137 |
+
"Identify fine-grained actions and explain how they move the task forward.\n"
|
| 138 |
+
"List relevant objects, tools, and environmental context, emphasizing functional states and transformations.\n"
|
| 139 |
+
"Note cues—repetition, transitions, or completion indicators—that situate the action in the procedural script.\n"
|
| 140 |
+
"</observation>\n"
|
| 141 |
+
"<reasoning>\n"
|
| 142 |
+
"Think through the question as a human would, Engage in an internal dialogue using expressions such as 'let me think', 'wait', 'hmm', 'oh, I see', 'let's break it down', etc.\n"
|
| 143 |
+
"Connect observations to the procedural plan to determine which step is being executed, progress, correctness, or anomalies.\n"
|
| 144 |
+
"Reflect on assumptions, verify interpretations, and, if appropriate, predict the agent's next likely action.\n"
|
| 145 |
+
"Synthesize understanding of what the agent is doing, how it fits into the broader task, and whether the process seems successful.\n"
|
| 146 |
+
"You are encouraged to include self-reflection or verification in your reasoning process.\n"
|
| 147 |
+
"</reasoning>\n"
|
| 148 |
+
"</think>\n"
|
| 149 |
+
"<answer>\n"
|
| 150 |
+
"[Final answer here — strictly follow the `{question_type}` output format and include no extra commentary.]\n"
|
| 151 |
+
"</answer>"
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
TYPE_TEMPLATE = {
|
| 155 |
+
"multiple choice": " Please provide only the single option letter (e.g., A, B, C, D, etc.) within the <answer> </answer> tags.",
|
| 156 |
+
"numerical": " Please provide the numerical value (e.g., 42 or 3.14) within the <answer> </answer> tags.",
|
| 157 |
+
"OCR": " Please transcribe text from the image/video clearly and provide your text answer within the <answer> </answer> tags.",
|
| 158 |
+
"free-form": " Please provide your text answer within the <answer> </answer> tags.",
|
| 159 |
+
"regression": " Please provide the numerical value (e.g., 42 or 3.14) within the <answer> </answer> tags.",
|
| 160 |
+
"boolean": " Please provide only 'Yes' or 'No' as your answer within the <answer> </answer> tags.",
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def extract_first_and_last_frame(path):
|
| 165 |
+
"""Initial state = first frame, current state = last decodable frame."""
|
| 166 |
+
cap = cv2.VideoCapture(path)
|
| 167 |
+
ok, first = cap.read()
|
| 168 |
+
if not ok:
|
| 169 |
+
raise RuntimeError(f"cannot read {path}")
|
| 170 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 171 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, total - 1))
|
| 172 |
+
ok, last = cap.read()
|
| 173 |
+
if not ok: # some containers mis-report the count
|
| 174 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, total - 2))
|
| 175 |
+
ok, last = cap.read()
|
| 176 |
+
cap.release()
|
| 177 |
+
to_pil = lambda f: Image.fromarray(cv2.cvtColor(f, cv2.COLOR_BGR2RGB))
|
| 178 |
+
return to_pil(first), to_pil(last if ok else first)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
init_img, current_img = extract_first_and_last_frame(video_path)
|
| 182 |
+
|
| 183 |
+
messages = [
|
| 184 |
+
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
|
| 185 |
+
{
|
| 186 |
+
"role": "user",
|
| 187 |
+
"content": [
|
| 188 |
+
{"type": "image", "image": init_img}, # 1. initial state
|
| 189 |
+
{"type": "video", "video": video_path, "nframes": 22}, # 2. the clip
|
| 190 |
+
{"type": "image", "image": current_img}, # 3. current state
|
| 191 |
+
{
|
| 192 |
+
"type": "text",
|
| 193 |
+
"text": QUESTION_TEMPLATE.format(
|
| 194 |
+
Question=question, question_type=problem_type
|
| 195 |
+
) + TYPE_TEMPLATE[problem_type],
|
| 196 |
+
},
|
| 197 |
+
],
|
| 198 |
+
},
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
llm = LLM(
|
| 202 |
+
model=MODEL_PATH,
|
| 203 |
+
tensor_parallel_size=torch.cuda.device_count(),
|
| 204 |
+
max_model_len=16384,
|
| 205 |
+
gpu_memory_utilization=0.8,
|
| 206 |
+
limit_mm_per_prompt={"image": 3, "video": 1}, # 2 anchor frames + 1 video
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
# top_p must stay this low. Larger values produce garbled output on this model.
|
| 210 |
+
sampling_params = SamplingParams(temperature=0.1, top_p=0.001, max_tokens=4096)
|
| 211 |
+
|
| 212 |
+
processor = AutoProcessor.from_pretrained(MODEL_PATH)
|
| 213 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
|
| 214 |
+
tokenizer.padding_side = "left"
|
| 215 |
+
processor.tokenizer = tokenizer
|
| 216 |
+
|
| 217 |
+
prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 218 |
+
image_inputs, video_inputs, video_kwargs = process_vision_info(messages, return_video_kwargs=True)
|
| 219 |
+
|
| 220 |
+
mm_data = {"video": video_inputs[0]}
|
| 221 |
+
if image_inputs:
|
| 222 |
+
mm_data["image"] = image_inputs
|
| 223 |
+
|
| 224 |
+
outputs = llm.generate(
|
| 225 |
+
[{
|
| 226 |
+
"prompt": prompt,
|
| 227 |
+
"multi_modal_data": mm_data,
|
| 228 |
+
"mm_processor_kwargs": {k: v[0] for k, v in video_kwargs.items()},
|
| 229 |
+
}],
|
| 230 |
+
sampling_params=sampling_params,
|
| 231 |
+
)
|
| 232 |
+
print(outputs[0].outputs[0].text)
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
### Parsing the output
|
| 236 |
+
|
| 237 |
+
```python
|
| 238 |
+
import re
|
| 239 |
+
|
| 240 |
+
def extract_answer(text):
|
| 241 |
+
m = re.search(r"<answer>\s*(.*?)\s*</answer>", text, re.DOTALL)
|
| 242 |
+
return m.group(1).strip() if m else ""
|
| 243 |
+
|
| 244 |
+
def extract_think(text):
|
| 245 |
+
m = re.search(r"<think>\s*(.*?)\s*</think>", text, re.DOTALL)
|
| 246 |
+
return m.group(1).strip() if m else ""
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
For `regression` and `numerical` questions the answer is a **progress percentage on a 0–100 scale** (`42.5`, not `0.425`). A trailing `%` is tolerated by the reference parser, which divides by 100 when it sees one.
|
| 250 |
+
|
| 251 |
+
### Question types
|
| 252 |
+
|
| 253 |
+
`regression` and `numerical` (progress estimation), `multiple choice`, `boolean` (failure detection), `free-form`, `OCR`. Pass the type both in `QUESTION_TEMPLATE` and via `TYPE_TEMPLATE`; the model was trained with both present.
|
| 254 |
+
|
| 255 |
+
### A note on frame counts
|
| 256 |
+
|
| 257 |
+
The published results were produced with the per-video frame cap at 22, so the launcher's `--nframes 32` effectively sampled 22. `nframes=22` above reproduces that. Training capped videos at 16 frames; eval samples more at higher resolution.
|
| 258 |
+
|
| 259 |
## Performance
|
| 260 |
|
| 261 |
+
Progress estimation averaged over four environments (paper Table 1):
|
| 262 |
+
|
| 263 |
+
| Model | Avg MRA ↑ | Avg MAE ↓ |
|
| 264 |
+
| --- | --- | --- |
|
| 265 |
+
| GPT-4o | 79.33 | 20.67 |
|
| 266 |
+
| GPT-5 mini | 75.38 | 23.96 |
|
| 267 |
+
| Qwen2.5-VL-72B | 73.80 | 23.80 |
|
| 268 |
+
| Qwen2.5-VL-7B (base) | 67.79 | 29.99 |
|
| 269 |
+
| InternVL 3.5 8B | 71.74 | 28.09 |
|
| 270 |
+
| ProgressLM | 78.32 | 20.87 |
|
| 271 |
+
| VLAC | 74.90 | 25.10 |
|
| 272 |
+
| **PRIMO R1 (7B)** | **82.90** | **15.52** |
|
| 273 |
+
|
| 274 |
+
Both training stages are needed (paper Table 2, MRA↑):
|
| 275 |
+
|
| 276 |
+
| Model | ID avg | OOD avg | Overall |
|
| 277 |
+
| --- | --- | --- | --- |
|
| 278 |
+
| Qwen2.5-VL-7B (base) | 70.38 | 65.26 | 67.46 |
|
| 279 |
+
| SFT only | 81.46 | 77.77 | 79.35 |
|
| 280 |
+
| RL only | 81.71 | 72.97 | 76.72 |
|
| 281 |
+
| **PRIMO R1 (SFT+RL)** | **88.47** | **82.90** | **85.28** |
|
| 282 |
+
|
| 283 |
+
RL without SFT underperforms because the model struggles to discover the output format from scratch; SFT alone generalizes poorly out of domain.
|
| 284 |
+
|
| 285 |
+
Zero-shot failure detection on RoboFail (paper Table 3): **67.0%**, matching Gemini 2.0 Flash and above GPT-4o at 63.0.
|
| 286 |
+
|
| 287 |
+
To reproduce these numbers, see [primo-bench-json](https://huggingface.co/datasets/LeonOverload/primo-bench-json), which documents the full evaluation path. One caveat when comparing against other work: the repo's harnesses score `regression` with three different formulas, and each output file records which one it used in its `regression_metric` field. The published numbers above come from `linear_relative_accuracy`.
|
| 288 |
|
| 289 |
## Citations
|
| 290 |
|