Instructions to use XinNUS/CycleGRPO-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use XinNUS/CycleGRPO-4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="XinNUS/CycleGRPO-4B") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("XinNUS/CycleGRPO-4B") model = AutoModelForMultimodalLM.from_pretrained("XinNUS/CycleGRPO-4B", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use XinNUS/CycleGRPO-4B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "XinNUS/CycleGRPO-4B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "XinNUS/CycleGRPO-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/XinNUS/CycleGRPO-4B
- SGLang
How to use XinNUS/CycleGRPO-4B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "XinNUS/CycleGRPO-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "XinNUS/CycleGRPO-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "XinNUS/CycleGRPO-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "XinNUS/CycleGRPO-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use XinNUS/CycleGRPO-4B with Docker Model Runner:
docker model run hf.co/XinNUS/CycleGRPO-4B
Fix model card: align import/clone to CycleGRPO repo (projects.transformers.vq_sam2)
Browse files
README.md
CHANGED
|
@@ -16,161 +16,70 @@ rewarded by how well the model can ground it back to the region it describes
|
|
| 16 |
stage. It produces descriptions with **interleaved segmentation masks** for the
|
| 17 |
corresponding parts of the answer, decoded through the SAMTok mask tokenizer.
|
| 18 |
|
|
|
|
|
|
|
| 19 |
## Quickstart
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
```bash
|
| 25 |
pip install "transformers>=4.57"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
```
|
| 30 |
|
| 31 |
-
###
|
| 32 |
|
| 33 |
```python
|
| 34 |
-
import re
|
| 35 |
import torch
|
| 36 |
-
import numpy as np
|
| 37 |
-
from PIL import Image
|
| 38 |
-
|
| 39 |
-
def extract_mt_token_ids_v1(text):
|
| 40 |
-
pattern = r"<\|mt_(\d{4})\|>"
|
| 41 |
-
return [int(x) for x in re.findall(pattern, text)]
|
| 42 |
-
|
| 43 |
-
def extract_mt_token_ids_v2(text):
|
| 44 |
-
pattern = re.compile(r'<\|mt_start\|><\|mt_(\d{4})\|><\|mt_(\d{4})\|><\|mt_end\|>')
|
| 45 |
-
matches = pattern.findall(text)
|
| 46 |
-
ret_list = []
|
| 47 |
-
for num1, num2 in matches:
|
| 48 |
-
ret_list.append(int(num1))
|
| 49 |
-
ret_list.append(int(num2))
|
| 50 |
-
return ret_list
|
| 51 |
-
|
| 52 |
-
def find_first_index(arr, value):
|
| 53 |
-
indices = np.where(arr == value)[0]
|
| 54 |
-
return indices[0] if len(indices) > 0 else -1
|
| 55 |
-
|
| 56 |
-
def fix_mt_format_comprehensive(text):
|
| 57 |
-
pattern_too_many = r'(<\|mt_start\|>)(<\|mt_\d+\|>)(<\|mt_\d+\|>)(?:<\|mt_\d+\|>)+<\|mt_end\|>'
|
| 58 |
-
replacement_too_many = r'\1\2\3<|mt_end|>'
|
| 59 |
-
text = re.sub(pattern_too_many, replacement_too_many, text)
|
| 60 |
-
|
| 61 |
-
pattern_too_few_with_end = r'(<\|mt_start\|>)(<\|mt_\d+\|>)(<\|mt_end\|>)'
|
| 62 |
-
replacement_too_few = r'\1\2<|mt_9999|><|mt_end|>'
|
| 63 |
-
text = re.sub(pattern_too_few_with_end, replacement_too_few, text)
|
| 64 |
-
|
| 65 |
-
pattern_too_few_no_end = r'(<\|mt_start\|>)(<\|mt_\d+\|>)(?!<\|mt_)'
|
| 66 |
-
replacement_too_few_no_end = r'\1\2<|mt_9999|><|mt_end|>'
|
| 67 |
-
text = re.sub(pattern_too_few_no_end, replacement_too_few_no_end, text)
|
| 68 |
-
return text
|
| 69 |
-
|
| 70 |
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
| 71 |
-
from projects.samtok.models import DirectResize, VQ_SAM2, VQ_SAM2Config, SAM2Config
|
| 72 |
-
|
| 73 |
-
CYCLEGRPO_REPO = "XinNUS/CycleGRPO-4B"
|
| 74 |
-
SAMTOK_REPO = "zhouyik/Qwen3-VL-4B-SAMTok" # mask tokenizer / SAM2 decoder assets are reused from the base repo
|
| 75 |
|
| 76 |
-
|
| 77 |
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 78 |
-
|
| 79 |
-
).
|
| 80 |
-
processor = AutoProcessor.from_pretrained(
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
codebook_size=CODEBOOK_SIZE,
|
| 91 |
-
codebook_depth=CODEBOOK_DEPTH,
|
| 92 |
-
shared_codebook=False,
|
| 93 |
-
latent_dim=256,
|
| 94 |
-
)
|
| 95 |
-
vq_sam2 = VQ_SAM2(vq_sam2_config).cuda().eval()
|
| 96 |
-
state = torch.load(f"{SAMTOK_REPO}/mask_tokenizer_256x2.pth", map_location="cpu")
|
| 97 |
-
vq_sam2.load_state_dict(state)
|
| 98 |
-
sam2_image_processor = DirectResize(1024)
|
| 99 |
-
|
| 100 |
-
# message
|
| 101 |
-
image_path = "figs/totoro.jpg"
|
| 102 |
-
question = "Could you please give me a detail description of the image? Please respond with interleaved segmentation masks for the corresponding parts of the answer."
|
| 103 |
-
image = Image.open(image_path).convert('RGB')
|
| 104 |
-
ori_width, ori_height = image.size
|
| 105 |
-
messages = [
|
| 106 |
-
{
|
| 107 |
-
"role": "user",
|
| 108 |
-
"content": [
|
| 109 |
-
{
|
| 110 |
-
"type": "image",
|
| 111 |
-
"image": image_path,
|
| 112 |
-
},
|
| 113 |
-
{"type": "text", "text": question},
|
| 114 |
-
],
|
| 115 |
-
}
|
| 116 |
-
]
|
| 117 |
-
|
| 118 |
-
# VLM inference
|
| 119 |
inputs = processor.apply_chat_template(
|
| 120 |
-
messages,
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
generated_ids = model.generate(
|
| 129 |
-
**inputs,
|
| 130 |
-
max_new_tokens=512,
|
| 131 |
-
do_sample=False,
|
| 132 |
-
top_p=1.0,
|
| 133 |
-
)
|
| 134 |
-
generated_ids_trimmed = [
|
| 135 |
-
out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
|
| 136 |
-
]
|
| 137 |
-
output_text = processor.batch_decode(
|
| 138 |
-
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
# decode mask
|
| 142 |
-
quant_ids = extract_mt_token_ids_v1(output_text[0])
|
| 143 |
-
if len(quant_ids) % CODEBOOK_DEPTH != 0:
|
| 144 |
-
output_text = [fix_mt_format_comprehensive(output_text[0])]
|
| 145 |
-
quant_ids = extract_mt_token_ids_v2(output_text[0])
|
| 146 |
-
|
| 147 |
-
batch_size = len(quant_ids) // CODEBOOK_DEPTH
|
| 148 |
-
remap_quant_ids = []
|
| 149 |
-
tags = []
|
| 150 |
-
for bs_id in range(batch_size):
|
| 151 |
-
chunk_quant_ids = quant_ids[bs_id*CODEBOOK_DEPTH:(bs_id+1)*CODEBOOK_DEPTH]
|
| 152 |
-
tags.append(f"{chunk_quant_ids[0]}-{chunk_quant_ids[1]}")
|
| 153 |
-
remap_chunk_quant_ids = [quant_id - book_id*CODEBOOK_SIZE for book_id, quant_id in enumerate(chunk_quant_ids)]
|
| 154 |
-
code1 = remap_chunk_quant_ids[0]
|
| 155 |
-
code2 = remap_chunk_quant_ids[1]
|
| 156 |
-
if not (code2 >= 0 and code2 < CODEBOOK_SIZE):
|
| 157 |
-
code2 = -1
|
| 158 |
-
remap_chunk_quant_ids_error_handle = [code1, code2]
|
| 159 |
-
remap_quant_ids.append(remap_chunk_quant_ids_error_handle)
|
| 160 |
-
|
| 161 |
-
batch_size = len(remap_quant_ids)
|
| 162 |
-
sam2_image = np.array(image)
|
| 163 |
-
sam2_image = sam2_image_processor.apply_image(sam2_image)
|
| 164 |
-
sam2_pixel_values = torch.from_numpy(sam2_image).permute(2, 0, 1).contiguous()
|
| 165 |
-
sam2_pixel_values = sam2_pixel_values.unsqueeze(0).to(vq_sam2.dtype).to(vq_sam2.device)
|
| 166 |
-
sam2_pixel_values = sam2_pixel_values.repeat(batch_size, 1, 1, 1)
|
| 167 |
-
|
| 168 |
-
quant_ids = torch.LongTensor(remap_quant_ids).to(vq_sam2.device)
|
| 169 |
-
|
| 170 |
-
with torch.no_grad():
|
| 171 |
-
_pred_masks = vq_sam2.forward_with_codes(sam2_pixel_values, quant_ids)
|
| 172 |
-
_pred_masks = torch.nn.functional.interpolate(_pred_masks, size=(ori_height, ori_width), mode='bilinear')
|
| 173 |
-
_pred_masks = _pred_masks > 0.5
|
| 174 |
-
_pred_masks = _pred_masks[:, 0, :, :].cpu().numpy().astype(np.uint8)
|
| 175 |
-
text_token_2d_mask_mapping = {tag: _pred_mask for tag, _pred_mask in zip(tags, _pred_masks)}
|
| 176 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
stage. It produces descriptions with **interleaved segmentation masks** for the
|
| 17 |
corresponding parts of the answer, decoded through the SAMTok mask tokenizer.
|
| 18 |
|
| 19 |
+
Code: [github.com/devinxzhang/CycleGRPO](https://github.com/devinxzhang/CycleGRPO)
|
| 20 |
+
|
| 21 |
## Quickstart
|
| 22 |
+
|
| 23 |
+
CycleGRPO-4B is a Qwen3-VL-4B that emits **SAMTok mask tokens** (`<|mt_...|>`). Plain
|
| 24 |
+
text generation works with 🤗 Transformers directly; turning the mask tokens into
|
| 25 |
+
segmentation masks needs the VQ-SAM2 decoder from the CycleGRPO repo
|
| 26 |
+
(`projects.transformers.vq_sam2`), so clone and install it first:
|
| 27 |
|
| 28 |
```bash
|
| 29 |
pip install "transformers>=4.57"
|
| 30 |
+
git clone https://github.com/devinxzhang/CycleGRPO.git
|
| 31 |
+
cd CycleGRPO # run from the repo root so `projects.transformers.vq_sam2` imports
|
| 32 |
+
pip install -e .
|
| 33 |
```
|
| 34 |
|
| 35 |
+
### Generate (text + mask tokens)
|
| 36 |
|
| 37 |
```python
|
|
|
|
| 38 |
import torch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
model_id = "XinNUS/CycleGRPO-4B"
|
| 42 |
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 43 |
+
model_id, dtype="auto", device_map="auto"
|
| 44 |
+
).eval()
|
| 45 |
+
processor = AutoProcessor.from_pretrained(model_id)
|
| 46 |
+
|
| 47 |
+
messages = [{
|
| 48 |
+
"role": "user",
|
| 49 |
+
"content": [
|
| 50 |
+
{"type": "image", "image": "figs/totoro.jpg"},
|
| 51 |
+
{"type": "text", "text": "Describe the image with interleaved segmentation "
|
| 52 |
+
"masks for the corresponding parts of the answer."},
|
| 53 |
+
],
|
| 54 |
+
}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
inputs = processor.apply_chat_template(
|
| 56 |
+
messages, tokenize=True, add_generation_prompt=True,
|
| 57 |
+
return_dict=True, return_tensors="pt",
|
| 58 |
+
).to(model.device)
|
| 59 |
+
|
| 60 |
+
out = model.generate(**inputs, max_new_tokens=512, do_sample=False)
|
| 61 |
+
text = processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]
|
| 62 |
+
print(text) # answer text interleaved with <|mt_start|><|mt_XXXX|><|mt_YYYY|><|mt_end|> mask tokens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
```
|
| 64 |
+
|
| 65 |
+
### Decode mask tokens → segmentation masks
|
| 66 |
+
|
| 67 |
+
The `<|mt_...|>` tokens are decoded to masks by the VQ-SAM2 mask tokenizer. Use the
|
| 68 |
+
**reference implementation in the CycleGRPO repo** rather than re-deriving it — see
|
| 69 |
+
`evaluation/groundingsuite/qwen3vl_groundingsuite_infer.py` (or
|
| 70 |
+
`evaluation/dlc_bench/inference.py`), which build the decoder and run the decode loop:
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
from projects.transformers.vq_sam2 import VQ_SAM2, VQ_SAM2Config, SAM2Config
|
| 74 |
+
# Those scripts also contain the `DirectResize` preprocessor, the mt-token parsing
|
| 75 |
+
# (extract_mt_token_ids / fix_mt_format), and the `VQ_SAM2.forward_with_codes(...)`
|
| 76 |
+
# decode step (codebook size 256, depth 2). Reuse them directly.
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
The decoder weights — `mask_tokenizer_256x2.pth` and `sam2.1_hiera_large.pt` — come
|
| 80 |
+
from the base model [Qwen3-VL-4B-SAMTok](https://huggingface.co/zhouyik/Qwen3-VL-4B-SAMTok).
|
| 81 |
+
|
| 82 |
+
## License
|
| 83 |
+
|
| 84 |
+
Released under Apache-2.0. Derived from Qwen3-VL-4B-SAMTok; use is also subject to the
|
| 85 |
+
base model's license and terms.
|