diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_clevr.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_clevr.sh
new file mode 100644
index 0000000000000000000000000000000000000000..b3730ebd20b5dfd8c485f8e9aa1085d3e3df8ecc
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_clevr.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -x
+
+export PYTHONUNBUFFERED=1
+
+MODEL_PATH=Qwen/Qwen2.5-VL-3B-Instruct # replace it with your local file path
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=BUAADreamer/clevr_count_70k@train \
+ data.val_files=BUAADreamer/clevr_count_70k@test \
+ data.format_prompt=./examples/format_prompt/r1v_format.jinja \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ worker.rollout.tensor_parallel_size=1 \
+ worker.reward.reward_type=sequential \
+ worker.reward.reward_function=./examples/reward_function/r1v.py:compute_score \
+ trainer.experiment_name=qwen2_5_vl_3b_clevr \
+ trainer.n_gpus_per_node=2
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_geoqa8k.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_geoqa8k.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c19c4fc477dbcf4faaa441c6ae9630bc59a0bcf8
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_geoqa8k.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -x
+
+export PYTHONUNBUFFERED=1
+
+MODEL_PATH=Qwen/Qwen2.5-VL-3B-Instruct # replace it with your local file path
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=leonardPKU/GEOQA_8K_R1V@train \
+ data.val_files=leonardPKU/GEOQA_8K_R1V@test \
+ data.format_prompt=./examples/format_prompt/r1v_format.jinja \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ worker.rollout.tensor_parallel_size=1 \
+ worker.reward.reward_type=sequential \
+ worker.reward.reward_function=./examples/reward_function/r1v.py:compute_score \
+ trainer.experiment_name=qwen2_5_vl_3b_geoqa8k \
+ trainer.n_gpus_per_node=8
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/config.yaml b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b3914d7b38f0161c9e9d8e5644fd64ad0ccde9f8
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/config.yaml
@@ -0,0 +1,96 @@
+data:
+ train_files: hiyouga/math12k@train
+ val_files: hiyouga/math12k@test
+ prompt_key: problem
+ answer_key: answer
+ image_key: image
+ max_prompt_length: 4096
+ max_response_length: 2048
+ rollout_batch_size: 256
+ val_batch_size: 1024
+ format_prompt: ./examples/format_prompt/math_format.jinja
+ override_chat_template: null
+ shuffle: true
+ seed: 1
+ max_pixels: 4194304
+ min_pixels: 262144
+ filter_overlong_prompts: true
+
+algorithm:
+ adv_estimator: grpo
+ disable_kl: false
+ use_kl_loss: true
+ kl_penalty: low_var_kl
+ kl_coef: 1.0e-2
+
+worker:
+ actor:
+ global_batch_size: 128
+ micro_batch_size_per_device_for_update: 4
+ micro_batch_size_per_device_for_experience: 8
+ max_grad_norm: 1.0
+ padding_free: true
+ ulysses_sequence_parallel_size: 1
+ is_entropy_reg: False
+ entropy_coef: 0.001
+ model:
+ model_path: Qwen/Qwen2.5-7B-Instruct
+ enable_gradient_checkpointing: true
+ trust_remote_code: true
+ freeze_vision_tower: false
+ optim:
+ lr: 1.0e-6
+ weight_decay: 1.0e-2
+ strategy: adamw # {adamw, adamw_bf16}
+ lr_warmup_ratio: 0.0
+ fsdp:
+ enable_full_shard: true
+ enable_cpu_offload: false
+ enable_rank0_init: true
+ offload:
+ offload_params: true # true: more CPU memory; false: more GPU memory
+ offload_optimizer: true # true: more CPU memory; false: more GPU memory
+
+ rollout:
+ n: 8
+ temperature: 1.0
+ top_p: 0.99
+ gpu_memory_utilization: 0.6
+ max_num_batched_tokens: 8192
+ max_model_len: 8192
+ enforce_eager: false
+ enable_chunked_prefill: false
+ tensor_parallel_size: 2
+ limit_images: 10
+ val_override_config:
+ temperature: 0.5
+ n: 1
+
+ ref:
+ fsdp:
+ enable_full_shard: true
+ enable_cpu_offload: true # true: more CPU memory; false: more GPU memory
+ enable_rank0_init: true
+ offload:
+ offload_params: false
+
+ reward:
+ reward_type: batch
+ reward_function: ./examples/reward_function/math.py:compute_score
+
+trainer:
+ total_epochs: 15
+ max_steps: null
+ project_name: easy_r1
+ experiment_name: qwen2_5_7b_math_grpo
+ logger: ["console", "wandb"]
+ nnodes: 1
+ n_gpus_per_node: 8
+ val_freq: -1 # -1 to disable
+ val_before_train: false
+ val_only: false
+ val_generations_to_log: 3
+ save_freq: 10 # -1 to disable
+ save_limit: -1 # -1 to disable
+ save_checkpoint_path: null
+ load_checkpoint_path: null
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/caption_format.jinja b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/caption_format.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..fefc131160f8c1908e227967359768a11fb0c381
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/caption_format.jinja
@@ -0,0 +1 @@
+{{ content | trim }} You FIRST describe the given image in a detailed manner, THEN think about the reasoning process as an internal monologue based on the generated description and given question and then provide the final answer. The description MUST BE enclosed within
tags. The reasoning process MUST BE enclosed within tags. The final answer MUST BE put in \boxed{}.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/math_format.jinja b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/math_format.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..8d6aa2344c57d654c87dbd26d90f7af473f82cc6
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/math_format.jinja
@@ -0,0 +1 @@
+{{ content | trim }} You FIRST think about the reasoning process as an internal monologue and then provide the final answer. The reasoning process MUST BE enclosed within tags. The final answer MUST BE put in \boxed{}.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/r1v_format.jinja b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/r1v_format.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..0ecf6f471146202113d4e44c10777d60e65c8e8a
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/r1v_format.jinja
@@ -0,0 +1 @@
+{{ content | trim }} A conversation between User and Assistant. The user asks a question, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process and answer are enclosed within and tags, respectively, i.e., reasoning process here answer here
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/llama3.2v_critic_grpo.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/llama3.2v_critic_grpo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..2010311223c8a5ec03bf27c9e3cd94a4f2e9f884
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/llama3.2v_critic_grpo.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+set -x
+
+export PYTHONUNBUFFERED=1
+
+MODEL_PATH=meta-llama/Llama-3.2-11B-Vision-Instruct # replace it with your local file path
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.shuffle=True \
+ data.format_prompt=./examples/format_prompt/math_format.jinja \
+ data.rollout_batch_size=256 \
+ data.max_prompt_length=4096 \
+ data.prompt_key=question \
+ worker.actor.is_entropy_reg=False \
+ worker.actor.global_batch_size=128 \
+ worker.actor.optim.lr=5.0e-7 \
+ worker.actor.micro_batch_size_per_device_for_update=4 \
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ worker.rollout.tensor_parallel_size=8 \
+ worker.rollout.limit_images=1 \
+ worker.rollout.gpu_memory_utilization=0.6 \
+ worker.reward.reward_function=./examples/reward_function/math.py:compute_score \
+ trainer.experiment_name=llama3.2v_critic_grpo \
+ trainer.n_gpus_per_node=8
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_7b_llava_critic_grpo.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_7b_llava_critic_grpo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..5823978189aeb09238635f7b9c32f5140f8e1ba3
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_7b_llava_critic_grpo.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+MODEL_PATH=XiaomiMiMo/MiMo-VL-7B-SFT-2508
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.filter_overlong_prompts=True \
+ data.prompt_key=question \
+ data.max_prompt_length=6142 \
+ worker.actor.is_entropy_reg=False \
+ worker.actor.global_batch_size=128 \
+ worker.actor.micro_batch_size_per_device_for_update=4 \
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ trainer.experiment_name=mimo_vl_7b_llava_critic_grpo \
+ trainer.n_gpus_per_node=8
+
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_rl_7b_llava_critic_grpo.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_rl_7b_llava_critic_grpo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c5d8bc37c0e8285ff08accabd6ba8676cb15f7e1
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_rl_7b_llava_critic_grpo.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+MODEL_PATH=XiaomiMiMo/MiMo-VL-7B-RL-2508
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.filter_overlong_prompts=True \
+ data.prompt_key=question \
+ data.max_prompt_length=6142 \
+ worker.actor.is_entropy_reg=False \
+ worker.actor.global_batch_size=128 \
+ worker.actor.micro_batch_size_per_device_for_update=4 \
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ trainer.experiment_name=mimo_vl_rl_7b_llava_critic_grpo \
+ trainer.n_gpus_per_node=8
+
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/qwen2_5_vl_7b_llava_critic_grpo.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/qwen2_5_vl_7b_llava_critic_grpo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..d91040467565c5cd542840e626451acba46942d4
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/qwen2_5_vl_7b_llava_critic_grpo.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+MODEL_PATH=Qwen/Qwen2.5-VL-7B-Instruct
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.filter_overlong_prompts=True \
+ data.prompt_key=question \
+ data.max_prompt_length=6142 \
+ worker.actor.is_entropy_reg=False \
+ worker.actor.global_batch_size=128 \
+ worker.actor.micro_batch_size_per_device_for_update=4 \
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ trainer.experiment_name=qwen2_5_vl_7b_llava_critic_grpo \
+ trainer.n_gpus_per_node=8
+
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/caption.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/caption.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c38f113afe2b1efe76118f5f4e4ce22c55253db
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/caption.py
@@ -0,0 +1,46 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Dict, List
+
+from mathruler.grader import extract_boxed_content, grade_answer
+
+
+def format_reward(predict: str) -> float:
+ pattern = re.compile(r".*.*.*.*\\boxed\{.*\}.*", re.DOTALL)
+ format_match = re.fullmatch(pattern, predict)
+ return 1.0 if format_match else 0.0
+
+
+def accuracy_reward(predict: str, ground_truth: str) -> float:
+ answer = extract_boxed_content(predict)
+ return 1.0 if grade_answer(answer, ground_truth) else 0.0
+
+
+def compute_score(predicts: List[str], ground_truths: List[str], format_weight: float = 0.1) -> List[Dict[str, float]]:
+ scores = []
+ for predict, ground_truth in zip(predicts, ground_truths):
+ predict = re.sub(r"\s*(<|>|/)\s*", r"\1", predict) # handle qwen2.5vl-32b format
+ format_score = format_reward(predict)
+ accuracy_score = accuracy_reward(predict, ground_truth)
+ scores.append(
+ {
+ "overall": (1 - format_weight) * accuracy_score + format_weight * format_score,
+ "format": format_score,
+ "accuracy": accuracy_score,
+ }
+ )
+
+ return scores
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/math.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/math.py
new file mode 100644
index 0000000000000000000000000000000000000000..f8887532df8cd0f86967c54e355839cb969066d6
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/math.py
@@ -0,0 +1,46 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Dict, List
+
+from mathruler.grader import extract_boxed_content, grade_answer
+
+
+def format_reward(predict: str) -> float:
+ pattern = re.compile(r".*.*\\boxed\{.*\}.*", re.DOTALL)
+ format_match = re.fullmatch(pattern, predict)
+ return 1.0 if format_match else 0.0
+
+
+def accuracy_reward(predict: str, ground_truth: str) -> float:
+ answer = extract_boxed_content(predict)
+ return 1.0 if grade_answer(answer, ground_truth) else 0.0
+
+
+def compute_score(predicts: List[str], ground_truths: List[str], format_weight: float = 0.1) -> List[Dict[str, float]]:
+ scores = []
+ for predict, ground_truth in zip(predicts, ground_truths):
+ predict = re.sub(r"\s*(<|>|/)\s*", r"\1", predict) # handle qwen2.5vl-32b format
+ format_score = format_reward(predict)
+ accuracy_score = accuracy_reward(predict, ground_truth)
+ scores.append(
+ {
+ "overall": (1 - format_weight) * accuracy_score + format_weight * format_score,
+ "format": format_score,
+ "accuracy": accuracy_score,
+ }
+ )
+
+ return scores
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/r1v.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/r1v.py
new file mode 100644
index 0000000000000000000000000000000000000000..204762f31458f8470ee0be7468cfc9ce5b627446
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/r1v.py
@@ -0,0 +1,47 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Dict
+
+from mathruler.grader import grade_answer
+
+
+def format_reward(predict: str) -> float:
+ pattern = re.compile(r".*?\s*.*?", re.DOTALL)
+ format_match = re.fullmatch(pattern, predict)
+ return 1.0 if format_match else 0.0
+
+
+def accuracy_reward(predict: str, ground_truth: str) -> float:
+ try:
+ content_match = re.search(r"(.*?)", predict)
+ given_answer = content_match.group(1).strip() if content_match else predict.strip()
+ if grade_answer(given_answer, ground_truth.strip()):
+ return 1.0
+
+ except Exception:
+ pass
+
+ return 0.0
+
+
+def compute_score(predict: str, ground_truth: str, format_weight: float = 0.5) -> Dict[str, float]:
+ format_score = format_reward(predict)
+ accuracy_score = accuracy_reward(predict, ground_truth)
+ return {
+ "overall": (1 - format_weight) * accuracy_score + format_weight * format_score,
+ "format": format_score,
+ "accuracy": accuracy_score,
+ }
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/runtime_env.yaml b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/runtime_env.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a4086253d4f7198f85225941c076285ab9f5321a
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/runtime_env.yaml
@@ -0,0 +1,9 @@
+working_dir: ./
+excludes: ["/.git/"]
+env_vars:
+ TOKENIZERS_PARALLELISM: "true"
+ NCCL_DEBUG: "WARN"
+ VLLM_LOGGING_LEVEL: "WARN"
+ TORCH_NCCL_AVOID_RECORD_STREAMS: "1"
+ PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:False"
+ PYTHONUNBUFFERED: "1"
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/thinklite_vl_7b_llava_critic_grpo.sh b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/thinklite_vl_7b_llava_critic_grpo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..13c4766819e1b89070bd2014c57d39ca39c93a33
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/examples/thinklite_vl_7b_llava_critic_grpo.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+MODEL_PATH=russwang/ThinkLite-VL-7B
+
+python3 -m verl.trainer.main \
+ config=examples/config.yaml \
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
+ data.filter_overlong_prompts=True \
+ data.prompt_key=question \
+ data.max_prompt_length=6142 \
+ worker.actor.is_entropy_reg=False \
+ worker.actor.global_batch_size=128 \
+ worker.actor.optim.lr=5.0e-7 \
+ worker.actor.micro_batch_size_per_device_for_update=4 \
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
+ worker.actor.model.model_path=${MODEL_PATH} \
+ trainer.experiment_name=thinklite_vl_7b_llava_critic_grpo \
+ trainer.n_gpus_per_node=8
\ No newline at end of file
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl.egg-info/PKG-INFO b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl.egg-info/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..08f0dd9c2a395c78e5f0089f3571ee6eb12ead1b
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl.egg-info/PKG-INFO
@@ -0,0 +1,262 @@
+Metadata-Version: 2.4
+Name: verl
+Version: 0.3.1.dev0
+Summary: An Efficient, Scalable, Multi-Modality RL Training Framework based on veRL
+Home-page: https://github.com/volcengine/verl
+Author: verl
+Author-email: zhangchi.usc1992@bytedance.com, gmsheng@connect.hku.hk, hiyouga@buaa.edu.cn
+License: Apache 2.0 License
+Requires-Python: >=3.9.0
+Description-Content-Type: text/markdown
+License-File: LICENSE
+Requires-Dist: accelerate
+Requires-Dist: codetiming
+Requires-Dist: datasets
+Requires-Dist: liger-kernel
+Requires-Dist: mathruler
+Requires-Dist: numpy
+Requires-Dist: omegaconf
+Requires-Dist: pandas
+Requires-Dist: peft
+Requires-Dist: pillow
+Requires-Dist: pyarrow>=15.0.0
+Requires-Dist: pylatexenc
+Requires-Dist: qwen-vl-utils
+Requires-Dist: ray[default]
+Requires-Dist: tensordict
+Requires-Dist: torchdata
+Requires-Dist: transformers>=4.51.0
+Requires-Dist: vllm>=0.7.3
+Requires-Dist: wandb
+Provides-Extra: dev
+Requires-Dist: pre-commit; extra == "dev"
+Requires-Dist: ruff; extra == "dev"
+Dynamic: author
+Dynamic: author-email
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
+
+# EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework
+
+[](https://github.com/hiyouga/EasyR1/stargazers)
+[](https://twitter.com/llamafactory_ai)
+
+This project is a clean fork of the original [veRL](https://github.com/volcengine/verl) project to support vision language models, we thank all the authors for providing such a high-performance RL training framework.
+
+EasyR1 is efficient and scalable due to the design of **[HybirdEngine](https://arxiv.org/abs/2409.19256)** and the latest release of **[vLLM](https://github.com/vllm-project/vllm)**'s SPMD mode.
+
+## Features
+
+- Supported models
+ - Llama3/Qwen2/Qwen2.5/Qwen3 language models
+ - Qwen2/Qwen2.5-VL vision language models
+ - DeepSeek-R1 distill models
+
+- Supported algorithms
+ - GRPO
+ - Reinforce++
+ - ReMax
+ - RLOO
+
+- Supported datasets
+ - Any text, vision-text dataset in a [specific format](#custom-dataset)
+
+- Supported tricks
+ - Padding-free training
+ - Resuming from checkpoint
+ - Wandb & SwanLab & Mlflow & Tensorboard tracking
+
+## Requirements
+
+### Software Requirements
+
+- Python 3.9+
+- transformers>=4.51.0
+- flash-attn>=2.4.3
+- vllm>=0.8.3
+
+We provide a [Dockerfile](./Dockerfile) to easily build environments.
+
+We recommend using the [pre-built docker image](https://hub.docker.com/r/hiyouga/verl) in EasyR1.
+
+```bash
+docker pull hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0
+```
+
+### Hardware Requirements
+
+\* *estimated*
+
+| Method | Bits | 1.5B | 3B | 7B | 32B |
+| ------------------------ | ---- | ------ | ------ | ------ | ------- |
+| GRPO Full Fine-Tuning | AMP | 2*24GB | 4*40GB | 8*40GB | 16*80GB |
+| GRPO Full Fine-Tuning | BF16 | 1*24GB | 1*40GB | 4*40GB | 8*80GB |
+
+> [!NOTE]
+> Use `worker.actor.fsdp.torch_dtype=bf16` and `worker.actor.optim.strategy=adamw_bf16` to enable bf16 training.
+>
+> We are working hard to reduce the VRAM in RL training, LoRA support will be integrated in next updates.
+
+## Tutorial: Run Qwen2.5-VL GRPO on [Geometry3K](https://huggingface.co/datasets/hiyouga/geometry3k) Dataset in Just 3 Steps
+
+
+
+### Installation
+
+```bash
+git clone https://github.com/hiyouga/EasyR1.git
+cd EasyR1
+pip install -e .
+```
+
+### GRPO Training
+
+```bash
+bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
+```
+
+### Merge Checkpoint in Hugging Face Format
+
+```bash
+python3 scripts/model_merger.py --local_dir checkpoints/easy_r1/exp_name/global_step_1/actor
+```
+
+> [!TIP]
+> If you encounter issues with connecting to Hugging Face, consider using `export HF_ENDPOINT=https://hf-mirror.com`.
+>
+> If you want to use SwanLab logger, consider using `bash examples/qwen2_5_vl_7b_geo3k_swanlab.sh`.
+
+## Custom Dataset
+
+Please refer to the example datasets to prepare your own dataset.
+
+- Text dataset: https://huggingface.co/datasets/hiyouga/math12k
+- Image-text dataset: https://huggingface.co/datasets/hiyouga/geometry3k
+- Multi-image-text dataset: https://huggingface.co/datasets/hiyouga/journeybench-multi-image-vqa
+
+## How to Understand GRPO in EasyR1
+
+
+
+- To learn about the GRPO algorithm, you can refer to [Hugging Face's blog](https://huggingface.co/docs/trl/v0.16.1/en/grpo_trainer).
+
+## How to Run 70B+ Model in Multi-node Environment
+
+1. Start the Ray head node.
+
+```bash
+ray start --head --port=6379 --dashboard-host=0.0.0.0
+```
+
+2. Start the Ray worker node and connect to the head node.
+
+```bash
+ray start --address=:6379
+```
+
+3. Check the Ray resource pool.
+
+```bash
+ray status
+```
+
+4. Run training script on the Ray head node only.
+
+```bash
+bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
+```
+
+See the **[veRL's official doc](https://verl.readthedocs.io/en/latest/start/multinode.html)** for more details about multi-node training and Ray debugger.
+
+## Other Baselines
+
+We also reproduced the following two baselines of the [R1-V](https://github.com/deep-agent/R1-V) project.
+- [CLEVR-70k-Counting](examples/baselines/qwen2_5_vl_3b_clevr.sh): Train the Qwen2.5-VL-3B-Instruct model on counting problem.
+- [GeoQA-8k](examples/baselines/qwen2_5_vl_3b_geoqa8k.sh): Train the Qwen2.5-VL-3B-Instruct model on GeoQA problem.
+
+## Performance Baselines
+
+See [baselines.md](assets/baselines.md).
+
+## Awesome Work using EasyR1
+
+- **MMR1**: Advancing the Frontiers of Multimodal Reasoning. [![[code]](https://img.shields.io/github/stars/LengSicong/MMR1)](https://github.com/LengSicong/MMR1)
+- **Vision-R1**: Incentivizing Reasoning Capability in Multimodal Large Language Models. [![[code]](https://img.shields.io/github/stars/Osilly/Vision-R1)](https://github.com/Osilly/Vision-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2503.06749-blue)](https://arxiv.org/abs/2503.06749)
+- **Seg-Zero**: Reasoning-Chain Guided Segmentation via Cognitive Reinforcement. [![[code]](https://img.shields.io/github/stars/dvlab-research/Seg-Zero)](https://github.com/dvlab-research/Seg-Zero) [![[arxiv]](https://img.shields.io/badge/arxiv-2503.06520-blue)](https://arxiv.org/abs/2503.06520)
+- **MetaSpatial**: Reinforcing 3D Spatial Reasoning in VLMs for the Metaverse. [![[code]](https://img.shields.io/github/stars/PzySeere/MetaSpatial)](https://github.com/PzySeere/MetaSpatial) [![[arxiv]](https://img.shields.io/badge/arxiv-2503.18470-blue)](https://arxiv.org/abs/2503.18470)
+- **Temporal-R1**: Envolving Temporal Reasoning Capability into LMMs via Temporal Consistent Reward. [![[code]](https://img.shields.io/github/stars/appletea233/Temporal-R1)](https://github.com/appletea233/Temporal-R1)
+- **NoisyRollout**: Reinforcing Visual Reasoning with Data Augmentation. [![[code]](https://img.shields.io/github/stars/John-AI-Lab/NoisyRollout)](https://github.com/John-AI-Lab/NoisyRollout) [![[arxiv]](https://img.shields.io/badge/arxiv-2504.13055-blue)](https://arxiv.org/pdf/2504.13055)
+- **GUI-R1**: A Generalist R1-Style Vision-Language Action Model For GUI Agents. [![[code]](https://img.shields.io/github/stars/ritzz-ai/GUI-R1)](https://github.com/ritzz-ai/GUI-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2504.10458-blue)](https://arxiv.org/abs/2504.10458)
+- **R1-Track**: Direct Application of MLLMs to Visual Object Tracking via Reinforcement Learning. [![[code]](https://img.shields.io/github/stars/Wangbiao2/R1-Track)](https://github.com/Wangbiao2/R1-Track)
+- **VisionReasoner**: Unified Visual Perception and Reasoning via Reinforcement Learning. [![[code]](https://img.shields.io/github/stars/dvlab-research/VisionReasoner)](https://github.com/dvlab-research/VisionReasoner) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.12081-blue)](https://arxiv.org/abs/2505.12081)
+- **MM-UPT**: Unsupervised Post-Training for Multi-Modal LLM Reasoning via GRPO. [![[code]](https://img.shields.io/github/stars/waltonfuture/MM-UPT)](https://github.com/waltonfuture/MM-UPT) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.22453-blue)](https://arxiv.org/pdf/2505.22453)
+- **RL-with-Cold-Start**: Advancing Multimodal Reasoning via Reinforcement Learning with Cold Start. [![[code]](https://img.shields.io/github/stars/waltonfuture/RL-with-Cold-Start)](https://github.com/waltonfuture/RL-with-Cold-Start) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.22334-blue)](https://arxiv.org/pdf/2505.22334)
+- **ViGoRL**: Grounded Reinforcement Learning for Visual Reasoning. [![[code]](https://img.shields.io/github/stars/Gabesarch/grounded-rl)](https://github.com/Gabesarch/grounded-rl) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.22334-blue)](https://arxiv.org/abs/2505.23678)
+- **Revisual-R1**: Advancing Multimodal Reasoning: From Optimized Cold Start to Staged Reinforcement Learning. [![[code]](https://img.shields.io/github/stars/CSfufu/Revisual-R1)](https://github.com/CSfufu/Revisual-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2506.04207-blue)](https://arxiv.org/abs/2506.04207)
+- **SophiaVL-R1**: Reinforcing MLLMs Reasoning with Thinking Reward. [![[code]](https://img.shields.io/github/stars/kxfan2002/SophiaVL-R1)](https://github.com/kxfan2002/SophiaVL-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.17018-blue)](https://arxiv.org/abs/2505.17018)
+
+## TODO
+
+- Support LoRA (high priority).
+- Support ulysses parallelism for VLMs (middle priority).
+- Support more VLM architectures.
+
+> [!NOTE]
+> We will not provide scripts for supervised fine-tuning and inference in this project. If you have such requirements, we recommend using [LLaMA-Factory](https://github.com/hiyouga/LLaMA-Factory).
+
+### Known bugs
+
+These features are temporarily disabled for now, we plan to fix them one-by-one in the future updates.
+
+- Vision language models are not compatible with ulysses parallelism yet.
+
+## Discussion Group
+
+👋 Join our [WeChat group](assets/wechat.jpg).
+
+## FAQs
+
+> ValueError: Image features and image tokens do not match: tokens: 8192, features 9800
+
+Increase the `data.max_prompt_length` or reduce the `data.max_pixels`.
+
+> RuntimeError: CUDA Error: out of memory at /workspace/csrc/cumem_allocator.cpp:62
+
+Reduce the `worker.rollout.gpu_memory_utilization` and enable `worker.actor.offload.offload_params`.
+
+> RuntimeError: 0 active drivers ([]). There should only be one.
+
+Uninstall `deepspeed` from the current python environment.
+
+## Citation
+
+Core contributors: [Yaowei Zheng](https://github.com/hiyouga), [Junting Lu](https://github.com/AL-377), [Shenzhi Wang](https://github.com/Shenzhi-Wang), [Zhangchi Feng](https://github.com/BUAADreamer), [Dongdong Kuang](https://github.com/Kuangdd01) and Yuwen Xiong
+
+We also thank Guangming Sheng and Chi Zhang for helpful discussions.
+
+```bibtex
+@misc{zheng2025easyr1,
+ title = {EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework},
+ author = {Yaowei Zheng, Junting Lu, Shenzhi Wang, Zhangchi Feng, Dongdong Kuang, Yuwen Xiong},
+ howpublished = {\url{https://github.com/hiyouga/EasyR1}},
+ year = {2025}
+}
+```
+
+We recommend to also cite the original work.
+
+```bibtex
+@article{sheng2024hybridflow,
+ title = {HybridFlow: A Flexible and Efficient RLHF Framework},
+ author = {Guangming Sheng and Chi Zhang and Zilingfeng Ye and Xibin Wu and Wang Zhang and Ru Zhang and Yanghua Peng and Haibin Lin and Chuan Wu},
+ year = {2024},
+ journal = {arXiv preprint arXiv: 2409.19256}
+}
+```
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Dockerfile b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..8b22d8465ca6eb5291b20c913e21dde3d42ddbd2
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Dockerfile
@@ -0,0 +1,76 @@
+# Start from the NVIDIA official image (ubuntu-22.04 + cuda-12.6 + python-3.10)
+# https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/rel-24-08.html
+FROM nvcr.io/nvidia/pytorch:24.08-py3
+
+# Define environments
+ENV MAX_JOBS=32
+ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
+ENV DEBIAN_FRONTEND=noninteractive
+ENV NODE_OPTIONS=""
+ENV PIP_ROOT_USER_ACTION=ignore
+ENV HF_HUB_ENABLE_HF_TRANSFER="1"
+
+# Define installation arguments
+ARG APT_SOURCE=https://mirrors.tuna.tsinghua.edu.cn/ubuntu/
+ARG PIP_INDEX=https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
+
+# Set apt source
+RUN cp /etc/apt/sources.list /etc/apt/sources.list.bak && \
+ { \
+ echo "deb ${APT_SOURCE} jammy main restricted universe multiverse"; \
+ echo "deb ${APT_SOURCE} jammy-updates main restricted universe multiverse"; \
+ echo "deb ${APT_SOURCE} jammy-backports main restricted universe multiverse"; \
+ echo "deb ${APT_SOURCE} jammy-security main restricted universe multiverse"; \
+ } > /etc/apt/sources.list
+
+# Install systemctl
+RUN apt-get update && \
+ apt-get install -y -o Dpkg::Options::="--force-confdef" systemd && \
+ apt-get clean
+
+# Install tini
+RUN apt-get update && \
+ apt-get install -y tini && \
+ apt-get clean
+
+# Change pip source
+RUN pip config set global.index-url "${PIP_INDEX}" && \
+ pip config set global.extra-index-url "${PIP_INDEX}" && \
+ python -m pip install --upgrade pip
+
+# Uninstall nv-pytorch fork
+RUN pip uninstall -y torch torchvision torchaudio \
+ pytorch-quantization pytorch-triton torch-tensorrt \
+ transformer_engine flash_attn apex megatron-core \
+ xgboost opencv grpcio
+
+# Fix cv2
+RUN rm -rf /usr/local/lib/python3.10/dist-packages/cv2
+
+# Install torch-2.6.0+cu124 + vllm-0.8.4
+# torch-2.6.0+cu124: cxx11abi=False
+# torch-2.6.0+cu126: cxx11abi=True
+# see https://github.com/flashinfer-ai/flashinfer/issues/911
+RUN pip install --no-cache-dir "vllm==0.8.4" "torch==2.6.0" "torchvision==0.21.0" "torchaudio==2.6.0" tensordict torchdata \
+ "transformers[hf_xet]>=4.51.0" accelerate datasets peft hf-transfer \
+ "numpy<2.0.0" "pyarrow>=15.0.0" pandas \
+ ray[default] codetiming hydra-core pylatexenc qwen-vl-utils wandb liger-kernel mathruler \
+ pytest yapf py-spy pyext pre-commit ruff
+
+# Install flash-attn-2.7.4.post1 (cxx11abi=False)
+RUN wget -nv https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl && \
+ pip install --no-cache-dir flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
+
+# Install flashinfer-0.2.2.post1+cu124 (cxx11abi=False)
+# vllm-0.8.3 does not support flashinfer>=0.2.3
+# see https://github.com/vllm-project/vllm/pull/15777
+RUN wget -nv https://github.com/flashinfer-ai/flashinfer/releases/download/v0.2.2.post1/flashinfer_python-0.2.2.post1+cu124torch2.6-cp38-abi3-linux_x86_64.whl && \
+ pip install --no-cache-dir flashinfer_python-0.2.2.post1+cu124torch2.6-cp38-abi3-linux_x86_64.whl
+
+# Fix packages
+RUN pip uninstall -y pynvml nvidia-ml-py && \
+ pip install --no-cache-dir --upgrade "nvidia-ml-py>=12.560.30" "fastapi[standard]>=0.115.0" "optree>=0.13.0" "pydantic>=2.9" "grpcio>=1.62.1"
+
+# Reset pip config
+RUN pip config unset global.index-url && \
+ pip config unset global.extra-index-url
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Makefile b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..39c5b5b79f16a3109b98b29c26c1c102bc2c7854
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Makefile
@@ -0,0 +1,18 @@
+.PHONY: build commit quality style
+
+check_dirs := examples scripts verl setup.py
+
+build:
+ python3 setup.py sdist bdist_wheel
+
+commit:
+ pre-commit install
+ pre-commit run --all-files
+
+quality:
+ ruff check $(check_dirs)
+ ruff format --check $(check_dirs)
+
+style:
+ ruff check $(check_dirs) --fix
+ ruff format $(check_dirs)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/README.md b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..1209a0a5d0f015d8a0cf75c6f0ecf82762ca0f1f
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/README.md
@@ -0,0 +1,217 @@
+# EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework
+
+[](https://github.com/hiyouga/EasyR1/stargazers)
+[](https://twitter.com/llamafactory_ai)
+
+This project is a clean fork of the original [veRL](https://github.com/volcengine/verl) project to support vision language models, we thank all the authors for providing such a high-performance RL training framework.
+
+EasyR1 is efficient and scalable due to the design of **[HybirdEngine](https://arxiv.org/abs/2409.19256)** and the latest release of **[vLLM](https://github.com/vllm-project/vllm)**'s SPMD mode.
+
+## Features
+
+- Supported models
+ - Llama3/Qwen2/Qwen2.5/Qwen3 language models
+ - Qwen2/Qwen2.5-VL vision language models
+ - DeepSeek-R1 distill models
+
+- Supported algorithms
+ - GRPO
+ - Reinforce++
+ - ReMax
+ - RLOO
+
+- Supported datasets
+ - Any text, vision-text dataset in a [specific format](#custom-dataset)
+
+- Supported tricks
+ - Padding-free training
+ - Resuming from checkpoint
+ - Wandb & SwanLab & Mlflow & Tensorboard tracking
+
+## Requirements
+
+### Software Requirements
+
+- Python 3.9+
+- transformers>=4.51.0
+- flash-attn>=2.4.3
+- vllm>=0.8.3
+
+We provide a [Dockerfile](./Dockerfile) to easily build environments.
+
+We recommend using the [pre-built docker image](https://hub.docker.com/r/hiyouga/verl) in EasyR1.
+
+```bash
+docker pull hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0
+```
+
+### Hardware Requirements
+
+\* *estimated*
+
+| Method | Bits | 1.5B | 3B | 7B | 32B |
+| ------------------------ | ---- | ------ | ------ | ------ | ------- |
+| GRPO Full Fine-Tuning | AMP | 2*24GB | 4*40GB | 8*40GB | 16*80GB |
+| GRPO Full Fine-Tuning | BF16 | 1*24GB | 1*40GB | 4*40GB | 8*80GB |
+
+> [!NOTE]
+> Use `worker.actor.fsdp.torch_dtype=bf16` and `worker.actor.optim.strategy=adamw_bf16` to enable bf16 training.
+>
+> We are working hard to reduce the VRAM in RL training, LoRA support will be integrated in next updates.
+
+## Tutorial: Run Qwen2.5-VL GRPO on [Geometry3K](https://huggingface.co/datasets/hiyouga/geometry3k) Dataset in Just 3 Steps
+
+
+
+### Installation
+
+```bash
+git clone https://github.com/hiyouga/EasyR1.git
+cd EasyR1
+pip install -e .
+```
+
+### GRPO Training
+
+```bash
+bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
+```
+
+### Merge Checkpoint in Hugging Face Format
+
+```bash
+python3 scripts/model_merger.py --local_dir checkpoints/easy_r1/exp_name/global_step_1/actor
+```
+
+> [!TIP]
+> If you encounter issues with connecting to Hugging Face, consider using `export HF_ENDPOINT=https://hf-mirror.com`.
+>
+> If you want to use SwanLab logger, consider using `bash examples/qwen2_5_vl_7b_geo3k_swanlab.sh`.
+
+## Custom Dataset
+
+Please refer to the example datasets to prepare your own dataset.
+
+- Text dataset: https://huggingface.co/datasets/hiyouga/math12k
+- Image-text dataset: https://huggingface.co/datasets/hiyouga/geometry3k
+- Multi-image-text dataset: https://huggingface.co/datasets/hiyouga/journeybench-multi-image-vqa
+
+## How to Understand GRPO in EasyR1
+
+
+
+- To learn about the GRPO algorithm, you can refer to [Hugging Face's blog](https://huggingface.co/docs/trl/v0.16.1/en/grpo_trainer).
+
+## How to Run 70B+ Model in Multi-node Environment
+
+1. Start the Ray head node.
+
+```bash
+ray start --head --port=6379 --dashboard-host=0.0.0.0
+```
+
+2. Start the Ray worker node and connect to the head node.
+
+```bash
+ray start --address=:6379
+```
+
+3. Check the Ray resource pool.
+
+```bash
+ray status
+```
+
+4. Run training script on the Ray head node only.
+
+```bash
+bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
+```
+
+See the **[veRL's official doc](https://verl.readthedocs.io/en/latest/start/multinode.html)** for more details about multi-node training and Ray debugger.
+
+## Other Baselines
+
+We also reproduced the following two baselines of the [R1-V](https://github.com/deep-agent/R1-V) project.
+- [CLEVR-70k-Counting](examples/baselines/qwen2_5_vl_3b_clevr.sh): Train the Qwen2.5-VL-3B-Instruct model on counting problem.
+- [GeoQA-8k](examples/baselines/qwen2_5_vl_3b_geoqa8k.sh): Train the Qwen2.5-VL-3B-Instruct model on GeoQA problem.
+
+## Performance Baselines
+
+See [baselines.md](assets/baselines.md).
+
+## Awesome Work using EasyR1
+
+- **MMR1**: Advancing the Frontiers of Multimodal Reasoning. [![[code]](https://img.shields.io/github/stars/LengSicong/MMR1)](https://github.com/LengSicong/MMR1)
+- **Vision-R1**: Incentivizing Reasoning Capability in Multimodal Large Language Models. [![[code]](https://img.shields.io/github/stars/Osilly/Vision-R1)](https://github.com/Osilly/Vision-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2503.06749-blue)](https://arxiv.org/abs/2503.06749)
+- **Seg-Zero**: Reasoning-Chain Guided Segmentation via Cognitive Reinforcement. [![[code]](https://img.shields.io/github/stars/dvlab-research/Seg-Zero)](https://github.com/dvlab-research/Seg-Zero) [![[arxiv]](https://img.shields.io/badge/arxiv-2503.06520-blue)](https://arxiv.org/abs/2503.06520)
+- **MetaSpatial**: Reinforcing 3D Spatial Reasoning in VLMs for the Metaverse. [![[code]](https://img.shields.io/github/stars/PzySeere/MetaSpatial)](https://github.com/PzySeere/MetaSpatial) [![[arxiv]](https://img.shields.io/badge/arxiv-2503.18470-blue)](https://arxiv.org/abs/2503.18470)
+- **Temporal-R1**: Envolving Temporal Reasoning Capability into LMMs via Temporal Consistent Reward. [![[code]](https://img.shields.io/github/stars/appletea233/Temporal-R1)](https://github.com/appletea233/Temporal-R1)
+- **NoisyRollout**: Reinforcing Visual Reasoning with Data Augmentation. [![[code]](https://img.shields.io/github/stars/John-AI-Lab/NoisyRollout)](https://github.com/John-AI-Lab/NoisyRollout) [![[arxiv]](https://img.shields.io/badge/arxiv-2504.13055-blue)](https://arxiv.org/pdf/2504.13055)
+- **GUI-R1**: A Generalist R1-Style Vision-Language Action Model For GUI Agents. [![[code]](https://img.shields.io/github/stars/ritzz-ai/GUI-R1)](https://github.com/ritzz-ai/GUI-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2504.10458-blue)](https://arxiv.org/abs/2504.10458)
+- **R1-Track**: Direct Application of MLLMs to Visual Object Tracking via Reinforcement Learning. [![[code]](https://img.shields.io/github/stars/Wangbiao2/R1-Track)](https://github.com/Wangbiao2/R1-Track)
+- **VisionReasoner**: Unified Visual Perception and Reasoning via Reinforcement Learning. [![[code]](https://img.shields.io/github/stars/dvlab-research/VisionReasoner)](https://github.com/dvlab-research/VisionReasoner) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.12081-blue)](https://arxiv.org/abs/2505.12081)
+- **MM-UPT**: Unsupervised Post-Training for Multi-Modal LLM Reasoning via GRPO. [![[code]](https://img.shields.io/github/stars/waltonfuture/MM-UPT)](https://github.com/waltonfuture/MM-UPT) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.22453-blue)](https://arxiv.org/pdf/2505.22453)
+- **RL-with-Cold-Start**: Advancing Multimodal Reasoning via Reinforcement Learning with Cold Start. [![[code]](https://img.shields.io/github/stars/waltonfuture/RL-with-Cold-Start)](https://github.com/waltonfuture/RL-with-Cold-Start) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.22334-blue)](https://arxiv.org/pdf/2505.22334)
+- **ViGoRL**: Grounded Reinforcement Learning for Visual Reasoning. [![[code]](https://img.shields.io/github/stars/Gabesarch/grounded-rl)](https://github.com/Gabesarch/grounded-rl) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.22334-blue)](https://arxiv.org/abs/2505.23678)
+- **Revisual-R1**: Advancing Multimodal Reasoning: From Optimized Cold Start to Staged Reinforcement Learning. [![[code]](https://img.shields.io/github/stars/CSfufu/Revisual-R1)](https://github.com/CSfufu/Revisual-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2506.04207-blue)](https://arxiv.org/abs/2506.04207)
+- **SophiaVL-R1**: Reinforcing MLLMs Reasoning with Thinking Reward. [![[code]](https://img.shields.io/github/stars/kxfan2002/SophiaVL-R1)](https://github.com/kxfan2002/SophiaVL-R1) [![[arxiv]](https://img.shields.io/badge/arxiv-2505.17018-blue)](https://arxiv.org/abs/2505.17018)
+
+## TODO
+
+- Support LoRA (high priority).
+- Support ulysses parallelism for VLMs (middle priority).
+- Support more VLM architectures.
+
+> [!NOTE]
+> We will not provide scripts for supervised fine-tuning and inference in this project. If you have such requirements, we recommend using [LLaMA-Factory](https://github.com/hiyouga/LLaMA-Factory).
+
+### Known bugs
+
+These features are temporarily disabled for now, we plan to fix them one-by-one in the future updates.
+
+- Vision language models are not compatible with ulysses parallelism yet.
+
+## Discussion Group
+
+👋 Join our [WeChat group](assets/wechat.jpg).
+
+## FAQs
+
+> ValueError: Image features and image tokens do not match: tokens: 8192, features 9800
+
+Increase the `data.max_prompt_length` or reduce the `data.max_pixels`.
+
+> RuntimeError: CUDA Error: out of memory at /workspace/csrc/cumem_allocator.cpp:62
+
+Reduce the `worker.rollout.gpu_memory_utilization` and enable `worker.actor.offload.offload_params`.
+
+> RuntimeError: 0 active drivers ([]). There should only be one.
+
+Uninstall `deepspeed` from the current python environment.
+
+## Citation
+
+Core contributors: [Yaowei Zheng](https://github.com/hiyouga), [Junting Lu](https://github.com/AL-377), [Shenzhi Wang](https://github.com/Shenzhi-Wang), [Zhangchi Feng](https://github.com/BUAADreamer), [Dongdong Kuang](https://github.com/Kuangdd01) and Yuwen Xiong
+
+We also thank Guangming Sheng and Chi Zhang for helpful discussions.
+
+```bibtex
+@misc{zheng2025easyr1,
+ title = {EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework},
+ author = {Yaowei Zheng, Junting Lu, Shenzhi Wang, Zhangchi Feng, Dongdong Kuang, Yuwen Xiong},
+ howpublished = {\url{https://github.com/hiyouga/EasyR1}},
+ year = {2025}
+}
+```
+
+We recommend to also cite the original work.
+
+```bibtex
+@article{sheng2024hybridflow,
+ title = {HybridFlow: A Flexible and Efficient RLHF Framework},
+ author = {Guangming Sheng and Chi Zhang and Zilingfeng Ye and Xibin Wu and Wang Zhang and Ru Zhang and Yanghua Peng and Haibin Lin and Chuan Wu},
+ year = {2024},
+ journal = {arXiv preprint arXiv: 2409.19256}
+}
+```
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf49f9036ebecaddcc5e67e760a2c3f2cb518beb
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/__init__.py
@@ -0,0 +1,32 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+from .utils.py_functional import is_package_available
+
+
+if is_package_available("modelscope"):
+ from modelscope.utils.hf_util import patch_hub # type: ignore
+
+
+__version__ = "0.3.1.dev0"
+
+
+if os.getenv("USE_MODELSCOPE_HUB", "0").lower() in ["true", "y", "1"]:
+ # Patch hub to download models from modelscope to speed up.
+ if not is_package_available("modelscope"):
+ raise ImportError("You are using the modelscope hub, please install modelscope by `pip install modelscope`.")
+
+ patch_hub()
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/init b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/init
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/init
@@ -0,0 +1 @@
+
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/monkey_patch.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/monkey_patch.py
new file mode 100644
index 0000000000000000000000000000000000000000..b621513e107abdf1160780fcfd43a6b4be0f149c
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/monkey_patch.py
@@ -0,0 +1,32 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
+
+from .transformers.flash_attention_utils import flash_attention_forward
+from .transformers.qwen2_vl import qwen2_vl_attn_forward
+
+
+def apply_ulysses_patch(model_type: str) -> None:
+ if model_type in ("llama", "mllama", "gemma", "gemma2", "mistral", "qwen2", "qwen3", "qwen3_moe"):
+ ALL_ATTENTION_FUNCTIONS["flash_attention_2"] = flash_attention_forward
+ elif model_type in ("qwen2_vl", "qwen2_5_vl"):
+ from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import Qwen2_5_VLFlashAttention2
+ from transformers.models.qwen2_vl.modeling_qwen2_vl import Qwen2VLFlashAttention2
+
+ Qwen2VLFlashAttention2.forward = qwen2_vl_attn_forward
+ Qwen2_5_VLFlashAttention2.forward = qwen2_vl_attn_forward
+ else:
+ raise NotImplementedError(f"Model architecture {model_type} is not supported yet.")
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/flash_attention_utils.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/flash_attention_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbfa73edc5ed49d98ede3f11bbdeec53b354b9ce
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/flash_attention_utils.py
@@ -0,0 +1,191 @@
+# Copyright 2024 The Fairseq Authors and the HuggingFace Inc. team
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+# Based on https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/modeling_flash_attention_utils.py
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import inspect
+import os
+from typing import Optional, Tuple
+
+import torch
+import torch.distributed as dist
+from transformers.modeling_flash_attention_utils import _flash_attention_forward, fa_peft_integration_check
+from transformers.utils import is_flash_attn_2_available, is_flash_attn_greater_or_equal_2_10
+
+from ...utils.ulysses import (
+ gather_heads_scatter_seq,
+ gather_seq_scatter_heads,
+ get_ulysses_sequence_parallel_group,
+ get_ulysses_sequence_parallel_world_size,
+)
+
+
+if is_flash_attn_2_available():
+ from flash_attn import flash_attn_func, flash_attn_varlen_func
+
+ _flash_supports_window_size = "window_size" in inspect.signature(flash_attn_func).parameters
+ _flash_supports_deterministic = "deterministic" in inspect.signature(flash_attn_func).parameters
+ _flash_deterministic_enabled = os.environ.get("FLASH_ATTENTION_DETERMINISTIC", "0") == "1"
+ _flash_use_top_left_mask = not is_flash_attn_greater_or_equal_2_10()
+
+
+def prepare_fa2_from_position_ids(
+ query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, position_ids: torch.Tensor
+):
+ query = query.view(-1, query.size(-2), query.size(-1))
+ key = key.contiguous().view(-1, key.size(-2), key.size(-1))
+ value = value.contiguous().view(-1, value.size(-2), value.size(-1))
+ position_ids = position_ids.flatten()
+ indices_q = torch.arange(position_ids.size(0), device=position_ids.device, dtype=torch.int32)
+ cu_seqlens = torch.cat(
+ (
+ indices_q[position_ids == 0],
+ torch.tensor(position_ids.size(), device=position_ids.device, dtype=torch.int32),
+ )
+ )
+ max_length = cu_seqlens.diff().max() # use cu_seqlens to infer max_length for qwen2vl mrope
+ return (query, key, value, indices_q, (cu_seqlens, cu_seqlens), (max_length, max_length))
+
+
+def _custom_flash_attention_forward(
+ query_states: torch.Tensor,
+ key_states: torch.Tensor,
+ value_states: torch.Tensor,
+ attention_mask: Optional[torch.Tensor],
+ query_length: int,
+ is_causal: bool = True,
+ position_ids: Optional[torch.Tensor] = None,
+ sliding_window: Optional[int] = None,
+ use_top_left_mask: bool = False,
+ deterministic: Optional[bool] = None,
+ **kwargs,
+):
+ """
+ Patches flash attention forward to handle 3D position ids in mrope. (3, batch_size, seq_length)
+ """
+ if not use_top_left_mask:
+ causal = is_causal
+ else:
+ causal = is_causal and query_length != 1
+
+ # Assuming 4D tensors, key_states.shape[1] is the key/value sequence length (source length).
+ use_sliding_windows = (
+ _flash_supports_window_size and sliding_window is not None and key_states.shape[1] > sliding_window
+ )
+ flash_kwargs = {"window_size": (sliding_window, sliding_window)} if use_sliding_windows else {}
+
+ if _flash_supports_deterministic:
+ flash_kwargs["deterministic"] = deterministic if deterministic is not None else _flash_deterministic_enabled
+
+ if kwargs.get("softcap") is not None:
+ flash_kwargs["softcap"] = kwargs.pop("softcap")
+
+ query_states, key_states, value_states = fa_peft_integration_check(
+ query_states, key_states, value_states, target_dtype=torch.bfloat16
+ )
+
+ sp_size = get_ulysses_sequence_parallel_world_size()
+ if sp_size > 1:
+ # (batch_size, seq_length, num_head, head_size)
+ query_states = gather_seq_scatter_heads(query_states, seq_dim=1, head_dim=2)
+ key_states = gather_seq_scatter_heads(key_states, seq_dim=1, head_dim=2)
+ value_states = gather_seq_scatter_heads(value_states, seq_dim=1, head_dim=2)
+ position_ids_lst = [torch.empty_like(position_ids) for _ in range(sp_size)]
+ position_ids = dist.all_gather(position_ids_lst, position_ids, group=get_ulysses_sequence_parallel_group())
+ position_ids = torch.cat(position_ids_lst, dim=-1) # (..., batch_size, seq_length)
+
+ if position_ids is not None and position_ids.dim() == 3: # qwen2vl mrope
+ position_ids = position_ids[0]
+
+ if position_ids is not None and query_length != 1 and not (torch.diff(position_ids, dim=-1) >= 0).all():
+ batch_size = query_states.size(0)
+ query_states, key_states, value_states, _, cu_seq_lens, max_seq_lens = prepare_fa2_from_position_ids(
+ query_states, key_states, value_states, position_ids
+ )
+ cu_seqlens_q, cu_seqlens_k = cu_seq_lens
+ max_seqlen_in_batch_q, max_seqlen_in_batch_k = max_seq_lens
+ attn_output = flash_attn_varlen_func(
+ query_states,
+ key_states,
+ value_states,
+ cu_seqlens_q=cu_seqlens_q,
+ cu_seqlens_k=cu_seqlens_k,
+ max_seqlen_q=max_seqlen_in_batch_q,
+ max_seqlen_k=max_seqlen_in_batch_k,
+ dropout_p=kwargs.pop("dropout", 0.0),
+ softmax_scale=kwargs.pop("softmax_scale", None),
+ causal=causal,
+ **flash_kwargs,
+ )
+ attn_output = attn_output.view(batch_size, -1, attn_output.size(-2), attn_output.size(-1))
+ else:
+ attn_output = _flash_attention_forward(
+ query_states,
+ key_states,
+ value_states,
+ attention_mask,
+ query_length,
+ is_causal=is_causal,
+ sliding_window=sliding_window,
+ use_top_left_mask=use_top_left_mask,
+ deterministic=deterministic,
+ **kwargs,
+ ) # do not pass position_ids to old flash_attention_forward
+
+ if sp_size > 1:
+ # (batch_size, seq_length, num_head, head_size)
+ attn_output = gather_heads_scatter_seq(attn_output, head_dim=2, seq_dim=1)
+
+ return attn_output
+
+
+def flash_attention_forward(
+ module: torch.nn.Module,
+ query: torch.Tensor,
+ key: torch.Tensor,
+ value: torch.Tensor,
+ attention_mask: Optional[torch.Tensor],
+ dropout: float = 0.0,
+ scaling: Optional[float] = None,
+ sliding_window: Optional[int] = None,
+ softcap: Optional[float] = None,
+ **kwargs,
+) -> Tuple[torch.Tensor, None]:
+ # This is before the transpose
+ q_len = query.shape[2]
+
+ # FA2 uses non-transposed inputs
+ query = query.transpose(1, 2)
+ key = key.transpose(1, 2)
+ value = value.transpose(1, 2)
+
+ # FA2 always relies on the value set in the module, so remove it if present in kwargs to avoid passing it twice
+ kwargs.pop("is_causal", None)
+
+ attn_output = _custom_flash_attention_forward(
+ query,
+ key,
+ value,
+ attention_mask,
+ query_length=q_len,
+ is_causal=True,
+ dropout=dropout,
+ softmax_scale=scaling,
+ sliding_window=sliding_window,
+ softcap=softcap,
+ use_top_left_mask=_flash_use_top_left_mask,
+ **kwargs,
+ )
+
+ return attn_output, None
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/qwen2_vl.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/qwen2_vl.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4f15801061c474be5eeea6af3dc2d3c6e65906e
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/qwen2_vl.py
@@ -0,0 +1,189 @@
+# Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+# Based on:
+# https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/models/qwen2_vl/modeling_qwen2_vl.py
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Tuple
+
+import torch
+
+from .flash_attention_utils import flash_attention_forward
+
+
+try:
+ from transformers.models.qwen2_vl.modeling_qwen2_vl import (
+ Qwen2VLAttention,
+ apply_multimodal_rotary_pos_emb,
+ repeat_kv,
+ )
+ from transformers.models.qwen2_vl.processing_qwen2_vl import Qwen2VLProcessor
+except ImportError:
+ pass
+
+
+def get_rope_index(
+ processor: "Qwen2VLProcessor",
+ input_ids: torch.Tensor,
+ image_grid_thw: Optional[torch.Tensor] = None,
+ video_grid_thw: Optional[torch.Tensor] = None,
+ second_per_grid_ts: Optional[torch.Tensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+) -> torch.Tensor:
+ """
+ Gets the position ids for Qwen2-VL, it should be generated before sharding the sequence.
+ The batch dim has been removed and the input_ids should be a 1D tensor representing a single example.
+ https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1546
+ """
+ spatial_merge_size = processor.image_processor.merge_size
+ tokens_per_second = 2
+ image_token_id = processor.tokenizer.convert_tokens_to_ids("<|image_pad|>")
+ video_token_id = processor.tokenizer.convert_tokens_to_ids("<|video_pad|>")
+ vision_start_token_id = processor.tokenizer.convert_tokens_to_ids("<|vision_start|>")
+ if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None):
+ if attention_mask is None:
+ attention_mask = torch.ones_like(input_ids)
+
+ position_ids = torch.ones(3, input_ids.size(0), dtype=input_ids.dtype, device=input_ids.device) # (3, seqlen)
+ image_index, video_index = 0, 0
+ input_ids = input_ids[attention_mask == 1]
+ image_nums, video_nums = 0, 0
+ vision_start_indices = torch.argwhere(input_ids == vision_start_token_id)
+ vision_tokens = input_ids[vision_start_indices + 1]
+ image_nums = (vision_tokens == image_token_id).sum()
+ video_nums = (vision_tokens == video_token_id).sum()
+ input_tokens = input_ids.tolist()
+ llm_pos_ids_list: list = []
+ st = 0
+ remain_images, remain_videos = image_nums, video_nums
+ for _ in range(image_nums + video_nums):
+ if image_token_id in input_tokens and remain_images > 0:
+ ed_image = input_tokens.index(image_token_id, st)
+ else:
+ ed_image = len(input_tokens) + 1
+ if video_token_id in input_tokens and remain_videos > 0:
+ ed_video = input_tokens.index(video_token_id, st)
+ else:
+ ed_video = len(input_tokens) + 1
+ if ed_image < ed_video:
+ t, h, w = (
+ image_grid_thw[image_index][0],
+ image_grid_thw[image_index][1],
+ image_grid_thw[image_index][2],
+ )
+ second_per_grid_t = 0
+ image_index += 1
+ remain_images -= 1
+ ed = ed_image
+ else:
+ t, h, w = (
+ video_grid_thw[video_index][0],
+ video_grid_thw[video_index][1],
+ video_grid_thw[video_index][2],
+ )
+ if second_per_grid_ts is not None:
+ second_per_grid_t = second_per_grid_ts[video_index]
+ else:
+ second_per_grid_t = 1.0
+
+ video_index += 1
+ remain_videos -= 1
+ ed = ed_video
+
+ llm_grid_t, llm_grid_h, llm_grid_w = (
+ t.item(),
+ h.item() // spatial_merge_size,
+ w.item() // spatial_merge_size,
+ )
+ text_len = ed - st
+
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
+ llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
+
+ t_index = torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w)
+ t_index = (t_index * second_per_grid_t * tokens_per_second).long().flatten()
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten()
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten()
+ llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx)
+ st = ed + llm_grid_t * llm_grid_h * llm_grid_w
+
+ if st < len(input_tokens):
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
+ text_len = len(input_tokens) - st
+ llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
+
+ llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
+ position_ids[..., attention_mask == 1] = llm_positions.to(position_ids.device)
+ else:
+ if attention_mask is not None:
+ position_ids = attention_mask.long().cumsum(-1) - 1
+ position_ids.masked_fill_(attention_mask == 0, 1)
+ position_ids = position_ids.unsqueeze(0).expand(3, -1).to(input_ids.device)
+ else:
+ position_ids = torch.arange(input_ids.shape[1], device=input_ids.device).view(1, -1).expand(3, -1)
+
+ return position_ids
+
+
+def qwen2_vl_attn_forward(
+ self: "Qwen2VLAttention",
+ hidden_states: torch.Tensor,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # will become mandatory in v4.46
+ **kwargs,
+) -> Tuple[torch.Tensor, None, None]:
+ bsz, q_len, _ = hidden_states.size() # q_len = seq_length / sp_size
+ query_states = self.q_proj(hidden_states) # (batch_size, seq_length / sp_size, num_heads * head_size)
+ key_states = self.k_proj(hidden_states)
+ value_states = self.v_proj(hidden_states)
+
+ query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
+ key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
+ value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
+
+ # Because the input can be padded, the absolute sequence length depends on the max position id.
+ if position_embeddings is None:
+ cos, sin = self.rotary_emb(value_states, position_ids)
+ else:
+ cos, sin = position_embeddings
+
+ query_states, key_states = apply_multimodal_rotary_pos_emb(
+ query_states, key_states, cos, sin, self.rope_scaling["mrope_section"]
+ )
+ key_states = repeat_kv(key_states, self.num_key_value_groups)
+ value_states = repeat_kv(value_states, self.num_key_value_groups)
+ dropout_rate = 0.0 if not self.training else self.attention_dropout
+
+ sliding_window = None
+ if (
+ self.config.use_sliding_window
+ and getattr(self.config, "sliding_window", None) is not None
+ and self.layer_idx >= self.config.max_window_layers
+ ):
+ sliding_window = self.config.sliding_window
+
+ attn_output, _ = flash_attention_forward(
+ self,
+ query_states,
+ key_states,
+ value_states,
+ attention_mask,
+ dropout=dropout_rate,
+ sliding_window=sliding_window,
+ position_ids=position_ids, # important: pass position ids
+ ) # (batch_size, seq_length, num_head / sp_size, head_size)
+ attn_output = attn_output.reshape(bsz, q_len, self.hidden_size).contiguous()
+ attn_output = self.o_proj(attn_output)
+ return attn_output, None, None
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/protocol.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/protocol.py
new file mode 100644
index 0000000000000000000000000000000000000000..65d48be0d82279099b2bddb85f8d810856325c8a
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/protocol.py
@@ -0,0 +1,689 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Implement base data transfer protocol between any two functions, modules.
+We can subclass Protocol to define more detailed batch info with specific keys
+"""
+
+import copy
+import io
+import pickle
+from collections import defaultdict
+from dataclasses import dataclass, field
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+import numpy as np
+import ray
+import torch
+from numpy.typing import NDArray
+from tensordict import TensorDict
+from torch.distributed import ProcessGroup
+from torch.utils.data import DataLoader
+
+from .utils.py_functional import union_two_dict
+
+
+try:
+ import tensordict
+
+ tensordict.set_lazy_legacy(False).set()
+except Exception:
+ pass
+
+
+__all__ = ["DataProto", "union_tensor_dict"]
+
+
+def pad_dataproto_to_divisor(data: "DataProto", size_divisor: int) -> Tuple["DataProto", int]:
+ """Pad a DataProto to size divisible by size_divisor
+
+ Args:
+ data (DataProto): the unpadded DataProto
+ size_divisor (int): size divisor
+
+ Returns:
+ data (DataProto): the padded DataProto
+ pad_size (int)
+ """
+ assert isinstance(data, DataProto), "data must be a DataProto"
+ if len(data) % size_divisor != 0:
+ pad_size = size_divisor - len(data) % size_divisor
+ padding_protos = []
+ remaining_pad = pad_size
+ while remaining_pad > 0:
+ take_size = min(remaining_pad, len(data))
+ padding_protos.append(data[:take_size])
+ remaining_pad -= take_size
+
+ data_padded = DataProto.concat([data] + padding_protos)
+ else:
+ pad_size = 0
+ data_padded = data
+
+ return data_padded, pad_size
+
+
+def unpad_dataproto(data: "DataProto", pad_size: int) -> "DataProto":
+ if pad_size != 0:
+ data = data[:-pad_size]
+
+ return data
+
+
+def union_tensor_dict(tensor_dict1: TensorDict, tensor_dict2: TensorDict) -> TensorDict:
+ """Union two tensordicts."""
+ if tensor_dict1.batch_size != tensor_dict2.batch_size:
+ raise ValueError(
+ f"Two tensor dict must have identical batch size. Got {tensor_dict1.batch_size} and {tensor_dict2.batch_size}"
+ )
+
+ for key in tensor_dict2.keys():
+ if key in tensor_dict1 and not torch.equal(tensor_dict1[key], tensor_dict2[key]):
+ raise ValueError(f"Key already exists: {key}.")
+
+ tensor_dict1[key] = tensor_dict2[key]
+
+ return tensor_dict1
+
+
+def union_numpy_dict(tensor_dict1: Dict[str, NDArray], tensor_dict2: Dict[str, NDArray]) -> Dict[str, NDArray]:
+ for key in tensor_dict2.keys():
+ if key in tensor_dict1:
+ assert isinstance(tensor_dict2[key], np.ndarray)
+ assert isinstance(tensor_dict1[key], np.ndarray)
+ if not np.all(tensor_dict1[key] == tensor_dict2[key]):
+ raise ValueError(f"Key already exists: {key}.")
+
+ tensor_dict1[key] = tensor_dict2[key]
+
+ return tensor_dict1
+
+
+def batch_collate(features: List[Dict[str, Any]]) -> Dict[str, List[Any]]:
+ if len(features) == 0:
+ return {}
+
+ batch_features = defaultdict(list)
+ for feature in features:
+ for key, value in feature.items():
+ batch_features[key].append(value)
+
+ return batch_features
+
+
+def fold_batch_dim(data: "DataProto", new_batch_size: int):
+ """
+ Fold a batch dim from [bsz, xxx] into [new_bsz, bsz // new_bsz, xxx]
+ """
+ batch_size = data.batch.batch_size[0]
+
+ assert batch_size % new_batch_size == 0
+
+ tensor: TensorDict = data.batch
+ non_tensor = data.non_tensor_batch
+
+ tensor = tensor.view(new_batch_size, -1)
+ tensor.auto_batch_size_(batch_dims=1)
+
+ for key, value in non_tensor.items():
+ non_tensor[key] = np.reshape(value, newshape=(new_batch_size, -1, *value.shape[1:]))
+
+ return DataProto(batch=tensor, non_tensor_batch=non_tensor, meta_info=data.meta_info)
+
+
+def collate_fn(data_items: list["DataProtoItem"]):
+ batch = []
+ non_tensor_batch = []
+ for data in data_items:
+ batch.append(data.batch)
+ non_tensor_batch.append(data.non_tensor_batch)
+
+ batch = torch.stack(batch).contiguous()
+ non_tensor_batch = batch_collate(non_tensor_batch)
+ non_tensor_batch = {key: np.array(value, dtype=object) for key, value in non_tensor_batch.items()}
+ return DataProto(batch=batch, non_tensor_batch=non_tensor_batch)
+
+
+@dataclass
+class DataProtoItem:
+ batch: Optional[TensorDict] = None
+ non_tensor_batch: Dict[str, NDArray] = field(default_factory=dict)
+ meta_info: Dict[str, Any] = field(default_factory=dict)
+
+
+@dataclass
+class DataProto:
+ """
+ A DataProto is a data structure that aims to provide a standard protocol for data exchange between functions.
+ It contains a batch (TensorDict) and a meta_info (Dict). The batch is a TensorDict https://pytorch.org/tensordict/.
+ TensorDict allows you to manipulate a dictionary of Tensors like a single Tensor. Ideally, the tensors with the
+ same batch size should be put inside batch.
+ """
+
+ batch: Optional[TensorDict] = None
+ non_tensor_batch: Dict[str, NDArray] = field(default_factory=dict)
+ meta_info: Dict[str, Any] = field(default_factory=dict)
+
+ def __post_init__(self):
+ self.check_consistency() # perform necessary checking
+
+ def __len__(self) -> int:
+ if self.batch is not None:
+ return self.batch.batch_size[0]
+ elif self.non_tensor_batch is not None and len(self.non_tensor_batch) > 0:
+ pivot_key = list(self.non_tensor_batch.keys())[0]
+ return self.non_tensor_batch[pivot_key].shape[0]
+ else:
+ return 0
+
+ def __getitem__(self, item: Union[int, slice]) -> Union["DataProto", "DataProtoItem"]:
+ tensor_data = self.batch[item]
+ non_tensor_data = {key: value[item] for key, value in self.non_tensor_batch.items()}
+ return_type = DataProto if isinstance(item, slice) else DataProtoItem
+ return return_type(batch=tensor_data, non_tensor_batch=non_tensor_data, meta_info=self.meta_info)
+
+ def __getstate__(self) -> Tuple[bytes, Dict[str, NDArray], Dict[str, Any]]:
+ buffer = io.BytesIO()
+ if self.batch is not None:
+ self.batch: TensorDict = self.batch.contiguous()
+ self.batch: TensorDict = self.batch.consolidate()
+
+ torch.save(self.batch, buffer)
+ buffer_bytes = buffer.getvalue()
+ return buffer_bytes, self.non_tensor_batch, self.meta_info
+
+ def __setstate__(self, data: Tuple[bytes, Dict[str, NDArray], Dict[str, Any]]) -> None:
+ batch_deserialized_bytes, non_tensor_batch, meta_info = data
+ batch_deserialized = io.BytesIO(batch_deserialized_bytes)
+ batch = torch.load(batch_deserialized, weights_only=False, map_location="cpu")
+ self.batch = batch
+ self.non_tensor_batch = non_tensor_batch
+ self.meta_info = meta_info
+
+ def save_to_disk(self, filepath: str) -> None:
+ with open(filepath, "wb") as f:
+ pickle.dump(self, f)
+
+ @staticmethod
+ def load_from_disk(filepath: str) -> "DataProto":
+ with open(filepath, "rb") as f:
+ data = pickle.load(f)
+ return data
+
+ def print_size(self, prefix: str = "") -> None:
+ size_of_tensordict = 0
+ if self.batch is not None:
+ for tensor in self.batch.values():
+ if isinstance(tensor, torch.Tensor):
+ size_of_tensordict += tensor.element_size() * tensor.numel()
+
+ size_of_numpy_array = 0
+ for value in self.non_tensor_batch.values():
+ size_of_numpy_array += value.nbytes
+
+ size_of_numpy_array /= 1024**3
+ size_of_tensordict /= 1024**3
+
+ message = f"Size of tensordict: {size_of_tensordict} GB, size of non_tensor_batch: {size_of_numpy_array} GB."
+ print({prefix}, {message})
+
+ def check_consistency(self):
+ """Check the consistency of the DataProto. Mainly for batch and non_tensor_batch
+ We expose this function as a public one so that user can call themselves directly
+ """
+ if self.batch is not None:
+ assert len(self.batch.batch_size) == 1, "only support num_batch_dims=1"
+
+ if self.batch is not None and len(self.non_tensor_batch) != 0:
+ # TODO: we can actually lift this restriction if needed
+ assert len(self.batch.batch_size) == 1, "only support num_batch_dims=1 when non_tensor_batch is not empty."
+
+ batch_size = self.batch.batch_size[0]
+ for key, value in self.non_tensor_batch.items():
+ assert len(value) == batch_size, f"key {key} length {len(value)} is not equal to bsz {batch_size}."
+
+ @classmethod
+ def from_single_dict(
+ cls,
+ data: Dict[str, Union[torch.Tensor, NDArray]],
+ meta_info: Optional[Dict[str, Any]] = None,
+ ) -> "DataProto":
+ tensors, non_tensors = {}, {}
+ for key, value in data.items():
+ if isinstance(value, torch.Tensor):
+ tensors[key] = value
+ elif isinstance(value, np.ndarray):
+ non_tensors[key] = value
+ else:
+ raise ValueError(f"Unsupported type in data {type(value)}")
+
+ return DataProto.from_dict(tensors=tensors, non_tensors=non_tensors, meta_info=meta_info)
+
+ @classmethod
+ def from_dict(
+ cls,
+ tensors: Dict[str, torch.Tensor],
+ non_tensors: Dict[str, NDArray] = None,
+ meta_info: Optional[Dict[str, Any]] = None,
+ num_batch_dims: int = 1,
+ ) -> "DataProto":
+ """Create a DataProto from a dict of tensors. This assumes that
+ 1. All the tensor in tensors have the same dim0
+ 2. Only dim0 is the batch dim
+ """
+ assert len(tensors) > 0, "tensors must not be empty"
+ assert num_batch_dims > 0, "num_batch_dims must be greater than zero"
+ if non_tensors is not None:
+ assert num_batch_dims == 1, "only support num_batch_dims=1 when non_tensors is not None."
+
+ meta_info = meta_info or {}
+ non_tensors = non_tensors or {}
+ assert isinstance(non_tensors, dict), "non_tensors should be a dictionary."
+
+ # get and check batch size
+ batch_size = None
+ pivot_key = None
+ for key, tensor in tensors.items():
+ if batch_size is None:
+ batch_size = tensor.shape[:num_batch_dims]
+ pivot_key = key
+ else:
+ current_batch = tensor.shape[:num_batch_dims]
+ assert batch_size == current_batch, (
+ f"Not all the tensor in tensors have the same batch size with batch_dims={num_batch_dims}. "
+ f"Got {pivot_key} has {batch_size}, {key} has {current_batch}"
+ )
+
+ tensor_dict = TensorDict(source=tensors, batch_size=batch_size)
+ return cls(batch=tensor_dict, non_tensor_batch=non_tensors, meta_info=meta_info)
+
+ def to(self, device: torch.device) -> "DataProto":
+ """move the batch to device
+
+ Args:
+ device (torch.device, str): torch device
+
+ Returns:
+ DataProto: the current DataProto
+
+ """
+ if self.batch is not None:
+ self.batch = self.batch.to(device)
+
+ return self
+
+ def select(
+ self,
+ batch_keys: Optional[List[str]] = None,
+ non_tensor_batch_keys: Optional[List[str]] = None,
+ meta_info_keys: Optional[List[str]] = None,
+ deepcopy: bool = False,
+ ) -> "DataProto":
+ """Select a subset of the DataProto via batch_keys and meta_info_keys
+
+ Args:
+ batch_keys (list, optional): a list of strings indicating the keys in batch to select
+ meta_info_keys (list, optional): a list of keys indicating the meta info to select
+
+ Returns:
+ DataProto: the DataProto with the selected batch_keys and meta_info_keys
+ """
+ # TODO (zhangchi.usc1992) whether to copy
+ if batch_keys is not None:
+ batch_keys = tuple(batch_keys)
+ sub_batch = self.batch.select(*batch_keys)
+ else:
+ sub_batch = self.batch
+
+ if non_tensor_batch_keys is not None:
+ non_tensor_batch = {k: v for k, v in self.non_tensor_batch.items() if k in non_tensor_batch_keys}
+ else:
+ non_tensor_batch = self.non_tensor_batch
+
+ if deepcopy:
+ non_tensor_batch = copy.deepcopy(non_tensor_batch)
+
+ if meta_info_keys is not None:
+ sub_meta_info = {k: v for k, v in self.meta_info.items() if k in meta_info_keys}
+ else:
+ sub_meta_info = self.meta_info
+
+ if deepcopy:
+ sub_meta_info = copy.deepcopy(sub_meta_info)
+
+ return DataProto(batch=sub_batch, non_tensor_batch=non_tensor_batch, meta_info=sub_meta_info)
+
+ def pop(
+ self,
+ batch_keys: Optional[List[str]] = None,
+ non_tensor_batch_keys: Optional[List[str]] = None,
+ meta_info_keys: Optional[List[str]] = None,
+ ) -> "DataProto":
+ """Pop a subset of the DataProto via `batch_keys` and `meta_info_keys`
+
+ Args:
+ batch_keys (list, optional): a list of strings indicating the keys in batch to pop
+ meta_info_keys (list, optional): a list of keys indicating the meta info to pop
+
+ Returns:
+ DataProto: the DataProto with the poped batch_keys and meta_info_keys
+ """
+ assert batch_keys is not None
+ non_tensor_batch_keys = non_tensor_batch_keys or []
+ meta_info_keys = meta_info_keys or []
+
+ tensors = {}
+ for key in batch_keys:
+ tensors[key] = self.batch.pop(key)
+
+ non_tensors = {}
+ for key in non_tensor_batch_keys:
+ non_tensors[key] = self.non_tensor_batch.pop(key)
+
+ meta_info = {}
+ for key in meta_info_keys:
+ meta_info[key] = self.meta_info.pop(key)
+
+ return DataProto.from_dict(tensors=tensors, non_tensors=non_tensors, meta_info=meta_info)
+
+ def rename(
+ self, old_keys: Optional[Union[str, List[str]]] = None, new_keys: Optional[Union[str, List[str]]] = None
+ ) -> "DataProto":
+ """
+ Note that this function only rename the key in the batch
+ """
+
+ def validate_input(keys):
+ if keys is not None:
+ if isinstance(keys, str):
+ keys = [keys]
+ elif isinstance(keys, list):
+ pass
+ else:
+ raise TypeError(f"keys must be a list or a string, but got {type(keys)}")
+ return keys
+
+ old_keys = validate_input(old_keys)
+ new_keys = validate_input(new_keys)
+
+ if len(new_keys) != len(old_keys):
+ raise ValueError(
+ f"new_keys and old_keys must have the same length, but got {len(new_keys)} and {len(old_keys)}"
+ )
+
+ self.batch.rename_key_(tuple(old_keys), tuple(new_keys))
+
+ return self
+
+ def union(self, other: "DataProto") -> "DataProto":
+ """Union with another DataProto. Union batch and meta_info separately.
+ Throw an error if
+ - there are conflict keys in batch and they are not equal
+ - the batch size of two data batch is not the same
+ - there are conflict keys in meta_info and they are not the same.
+
+ Args:
+ other (DataProto): another DataProto to union
+
+ Returns:
+ DataProto: the DataProto after union
+ """
+ self.batch = union_tensor_dict(self.batch, other.batch)
+ self.non_tensor_batch = union_numpy_dict(self.non_tensor_batch, other.non_tensor_batch)
+ self.meta_info = union_two_dict(self.meta_info, other.meta_info)
+ return self
+
+ def make_iterator(
+ self, mini_batch_size: int, epochs: int, seed: int = None, dataloader_kwargs: Dict[str, Any] = None
+ ):
+ """Make an iterator from the DataProto. This is built upon that TensorDict can be used as a normal Pytorch
+ dataset. See https://pytorch.org/tensordict/tutorials/data_fashion for more details.
+
+ Args:
+ mini_batch_size (int): mini-batch size when iterating the dataset. We require that
+ ``batch.batch_size[0] % mini_batch_size == 0``
+ epochs (int): number of epochs when iterating the dataset.
+ dataloader_kwargs: internally, it returns a DataLoader over the batch.
+ The dataloader_kwargs is the kwargs passed to the DataLoader
+
+ Returns:
+ Iterator: an iterator that yields a mini-batch data at a time. The total number of iteration steps is
+ ``self.batch.batch_size * epochs // mini_batch_size``
+ """
+ assert self.batch.batch_size[0] % mini_batch_size == 0, f"{self.batch.batch_size[0]} % {mini_batch_size} != 0"
+ # we can directly create a dataloader from TensorDict
+ if dataloader_kwargs is None:
+ dataloader_kwargs = {}
+
+ if seed is not None:
+ generator = torch.Generator()
+ generator.manual_seed(seed)
+ else:
+ generator = None
+
+ assert isinstance(dataloader_kwargs, Dict)
+ train_dataloader = DataLoader(
+ dataset=self, batch_size=mini_batch_size, collate_fn=collate_fn, generator=generator, **dataloader_kwargs
+ )
+
+ def get_data():
+ for _ in range(epochs):
+ for d in train_dataloader:
+ d.meta_info = self.meta_info
+ yield d
+
+ return iter(get_data())
+
+ def chunk(self, chunks: int) -> List["DataProto"]:
+ """Split the batch among dim=0 into chunks. The meta_info is passed to each DataProto after split.
+
+ Args:
+ chunks (int): the number of chunks to split on dim=0
+
+ Returns:
+ List[DataProto]: a list of DataProto after splitting
+ """
+ assert len(self) % chunks == 0, (
+ f"only support equal chunk. Got size of DataProto {len(self)} and chunk {chunks}."
+ )
+ if self.batch is not None:
+ batch_lst = self.batch.chunk(chunks=chunks, dim=0)
+ else:
+ batch_lst = [None for _ in range(chunks)]
+
+ non_tensor_batch_lst = [{} for _ in range(chunks)]
+ for key, value in self.non_tensor_batch.items():
+ assert isinstance(value, np.ndarray)
+ non_tensor_lst = np.array_split(value, chunks)
+ assert len(non_tensor_lst) == chunks
+ for i in range(chunks):
+ non_tensor_batch_lst[i][key] = non_tensor_lst[i]
+
+ output = []
+ for i in range(chunks):
+ output.append(
+ DataProto(batch=batch_lst[i], non_tensor_batch=non_tensor_batch_lst[i], meta_info=self.meta_info)
+ )
+
+ return output
+
+ def split(self, split_size: int) -> List["DataProto"]:
+ chunks = len(self) // split_size
+ return self.chunk(chunks)
+
+ @staticmethod
+ def concat(data: List["DataProto"]) -> "DataProto":
+ """Concat a list of DataProto. The batch is concatenated among dim=0.
+ The meta_info is assumed to be identical and will use the first one.
+
+ Args:
+ data (List[DataProto]): list of DataProto
+
+ Returns:
+ DataProto: concatenated DataProto
+ """
+ batch_lst = [batch.batch for batch in data]
+ if batch_lst[0] is not None:
+ new_batch = torch.cat(batch_lst, dim=0)
+ else:
+ new_batch = None
+
+ non_tensor_batch = batch_collate([d.non_tensor_batch for d in data])
+ for key, value in non_tensor_batch.items():
+ non_tensor_batch[key] = np.concatenate(value, axis=0)
+
+ return DataProto(batch=new_batch, non_tensor_batch=non_tensor_batch, meta_info=data[0].meta_info)
+
+ def reorder(self, indices: torch.Tensor) -> None:
+ """
+ Note that this operation is in-place
+ """
+ indices_np = indices.detach().numpy()
+ self.batch = self.batch[indices]
+ self.non_tensor_batch = {key: value[indices_np] for key, value in self.non_tensor_batch.items()}
+
+ def repeat(self, repeat_times: int = 2, interleave: bool = True) -> "DataProto":
+ """
+ Repeat the batch data a specified number of times.
+
+ Args:
+ repeat_times (int): Number of times to repeat the data.
+ interleave (bool): Whether to interleave the repeated data.
+
+ Returns:
+ DataProto: A new DataProto with repeated data.
+ """
+ if self.batch is not None:
+ if interleave:
+ # Interleave the data
+ repeated_tensors = {
+ key: tensor.repeat_interleave(repeat_times, dim=0) for key, tensor in self.batch.items()
+ }
+ else:
+ # Stack the data
+ repeated_tensors = {
+ key: tensor.unsqueeze(0).expand(repeat_times, *tensor.shape).reshape(-1, *tensor.shape[1:])
+ for key, tensor in self.batch.items()
+ }
+
+ repeated_batch = TensorDict(
+ source=repeated_tensors,
+ batch_size=(self.batch.batch_size[0] * repeat_times,),
+ )
+ else:
+ repeated_batch = None
+
+ repeated_non_tensor_batch = {}
+ for key, value in self.non_tensor_batch.items():
+ if interleave:
+ repeated_non_tensor_batch[key] = np.repeat(value, repeat_times, axis=0)
+ else:
+ repeated_non_tensor_batch[key] = np.tile(value, (repeat_times,) + (1,) * (value.ndim - 1))
+
+ return DataProto(
+ batch=repeated_batch,
+ non_tensor_batch=repeated_non_tensor_batch,
+ meta_info=self.meta_info,
+ )
+
+
+@dataclass
+class DataProtoFuture:
+ """
+ DataProtoFuture aims to eliminate actual data fetching on driver. By doing so, the driver doesn't have to wait
+ for data so that asynchronous execution becomes possible.
+ DataProtoFuture contains a list of futures from another WorkerGroup of size world_size.
+ - collect_fn is a Callable that reduces the list of futures to a DataProto
+ - dispatch_fn is a Callable that partitions the DataProto into a list of DataProto of size world_size and then select
+
+ Potential issue: we can optimize dispatch_fn(collect_fn) such that only needed data is fetched on destination
+ - DataProtoFuture only supports directly passing from the output of a method to another input. You can't perform any
+ operation on the DataProtoFuture in driver.
+ """
+
+ collect_fn: Callable
+ futures: List[ray.ObjectRef]
+ dispatch_fn: Callable = None
+
+ @staticmethod
+ def concat(data: List[ray.ObjectRef]) -> "DataProtoFuture":
+ output = DataProtoFuture(collect_fn=DataProto.concat, futures=data)
+ return output
+
+ def chunk(self, chunks: int) -> List["DataProtoFuture"]:
+ from functools import partial
+
+ arg_future_lst = []
+ for i in range(chunks):
+ # note that we can't directly pass i and chunks
+ def dispatch_fn(x, i, chunks):
+ return x.chunk(chunks=chunks)[i]
+
+ arg_future = DataProtoFuture(
+ collect_fn=self.collect_fn, dispatch_fn=partial(dispatch_fn, i=i, chunks=chunks), futures=self.futures
+ )
+ arg_future_lst.append(arg_future)
+ return arg_future_lst
+
+ def get(self):
+ outputs = ray.get(self.futures) # dp_size.
+ for output in outputs:
+ assert isinstance(output, DataProto)
+
+ outputs = self.collect_fn(outputs) # select dp, concat
+ if self.dispatch_fn is not None:
+ outputs = self.dispatch_fn(outputs) # split in batch dim, select using dp
+
+ return outputs
+
+
+def allgather_dict_tensors(
+ tensors: Union[Dict[str, torch.Tensor], TensorDict], size: int, group: ProcessGroup, dim: int = 0
+) -> Union[Dict[str, torch.Tensor], TensorDict]:
+ """
+ TODO: optimize this.
+ - We can use async ops
+ - We can use only one allgather
+ """
+ if isinstance(tensors, TensorDict):
+ is_tensor_dict = True
+ tensors_as_dict = tensors.to_dict()
+ else:
+ tensors_as_dict = tensors
+ is_tensor_dict = False
+
+ output = {}
+ sorted_keys = sorted(tensors_as_dict.keys())
+ for key in sorted_keys:
+ value = tensors_as_dict[key]
+ output[key] = [torch.empty_like(value) for _ in range(size)]
+ torch.distributed.all_gather(output[key], value, group=group, async_op=False)
+ output[key] = torch.cat(output[key], dim=dim)
+
+ if is_tensor_dict:
+ output = TensorDict(source=output, batch_size=tensors.batch_size[0] * size)
+
+ return output
+
+
+def all_gather_data_proto(data: DataProto, size: int, group: ProcessGroup) -> None:
+ # Note that this is an inplace operator just like torch.distributed.all_gather
+ prev_device = data.batch.device
+ data.batch = data.batch.cuda(device=torch.cuda.current_device())
+ data.batch = allgather_dict_tensors(data.batch.contiguous(), size=size, group=group, dim=0)
+ data.batch = data.batch.to(prev_device)
+ # all gather non_tensor_batch
+ all_non_tensor_batch = [None for _ in range(size)]
+ torch.distributed.all_gather_object(all_non_tensor_batch, data.non_tensor_batch, group=group)
+ data.non_tensor_batch = {k: np.concatenate([d[k] for d in all_non_tensor_batch]) for k in data.non_tensor_batch}
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/pyproject.toml b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2f579c19b4ec15bfb975ea1ad1d266d00a516dfe
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/pyproject.toml
@@ -0,0 +1,39 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "verl"
+dynamic = [
+ "version",
+ "dependencies",
+ "optional-dependencies",
+ "requires-python",
+ "authors",
+ "description",
+ "readme",
+ "license"
+]
+
+[tool.ruff]
+target-version = "py39"
+line-length = 119
+indent-width = 4
+
+[tool.ruff.lint]
+ignore = ["C901", "E501", "E741", "W605", "C408"]
+select = ["C", "E", "F", "I", "W", "RUF022"]
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["E402", "F401", "F403", "F811"]
+
+[tool.ruff.lint.isort]
+lines-after-imports = 2
+known-first-party = ["verl"]
+known-third-party = ["torch", "transformers", "wandb"]
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "auto"
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/requirements.txt b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8543c231b9ec75fe9300417c1498d9328dfadbf2
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/requirements.txt
@@ -0,0 +1,19 @@
+accelerate
+codetiming
+datasets
+liger-kernel
+mathruler
+numpy
+omegaconf
+pandas
+peft
+pillow
+pyarrow>=15.0.0
+pylatexenc
+qwen-vl-utils
+ray[default]
+tensordict
+torchdata
+transformers>=4.51.0
+vllm>=0.7.3
+wandb
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..46c9670eebda2595b2ea248c640f9d533506c226
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .worker import Worker
+from .worker_group import ClassWithInitArgs, ResourcePool, WorkerGroup
+
+
+__all__ = ["ClassWithInitArgs", "ResourcePool", "Worker", "WorkerGroup"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/decorator.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/decorator.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0e85a36fbd52d115b68fbd39c2d9a44fa158358
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/decorator.py
@@ -0,0 +1,213 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import Enum, auto
+from functools import wraps
+from types import FunctionType
+from typing import TYPE_CHECKING, Dict, List, Literal, Union
+
+import ray
+
+from ...protocol import DataProto, DataProtoFuture
+
+
+if TYPE_CHECKING:
+ from .worker_group import WorkerGroup
+
+
+# here we add a magic number of avoid user-defined function already have this attribute
+MAGIC_ATTR = "attrs_3141562937"
+
+
+class Dispatch(Enum):
+ RANK_ZERO = auto()
+ ONE_TO_ALL = auto()
+ ALL_TO_ALL = auto()
+ DP_COMPUTE = auto()
+ DP_COMPUTE_PROTO = auto()
+ DP_COMPUTE_PROTO_WITH_FUNC = auto()
+ DP_COMPUTE_METRIC = auto()
+
+
+class Execute(Enum):
+ ALL = 0
+ RANK_ZERO = 1
+
+
+def _split_args_kwargs_data_proto(chunks: int, *args, **kwargs):
+ splitted_args = []
+ for arg in args:
+ assert isinstance(arg, (DataProto, DataProtoFuture))
+ splitted_args.append(arg.chunk(chunks=chunks))
+
+ splitted_kwargs = {}
+ for key, value in kwargs.items():
+ assert isinstance(value, (DataProto, DataProtoFuture))
+ splitted_kwargs[key] = value.chunk(chunks=chunks)
+
+ return splitted_args, splitted_kwargs
+
+
+def dispatch_one_to_all(worker_group: "WorkerGroup", *args, **kwargs):
+ args = tuple([arg] * worker_group.world_size for arg in args)
+ kwargs = {k: [v] * worker_group.world_size for k, v in kwargs.items()}
+ return args, kwargs
+
+
+def dispatch_all_to_all(worker_group: "WorkerGroup", *args, **kwargs):
+ return args, kwargs
+
+
+def collect_all_to_all(worker_group: "WorkerGroup", output):
+ return output
+
+
+def _concat_data_proto_or_future(outputs: List[DataProto]) -> DataProto:
+ # make sure all the elements in output has the same type
+ for output in outputs:
+ assert type(output) is type(outputs[0])
+
+ output = outputs[0]
+
+ if isinstance(output, DataProto):
+ return DataProto.concat(outputs)
+ elif isinstance(output, ray.ObjectRef):
+ return DataProtoFuture.concat(outputs)
+ else:
+ raise NotImplementedError
+
+
+def dispatch_dp_compute(worker_group: "WorkerGroup", *args, **kwargs):
+ for arg in args:
+ assert isinstance(arg, (tuple, list)) and len(arg) == worker_group.world_size
+
+ for value in kwargs.values():
+ assert isinstance(value, (tuple, list)) and len(value) == worker_group.world_size
+
+ return args, kwargs
+
+
+def collect_dp_compute(worker_group: "WorkerGroup", outputs: List[DataProto]) -> List[DataProto]:
+ assert len(outputs) == worker_group.world_size
+ return outputs
+
+
+def dispatch_dp_compute_data_proto(worker_group: "WorkerGroup", *args, **kwargs):
+ splitted_args, splitted_kwargs = _split_args_kwargs_data_proto(worker_group.world_size, *args, **kwargs)
+ return splitted_args, splitted_kwargs
+
+
+def dispatch_dp_compute_data_proto_with_func(worker_group: "WorkerGroup", *args, **kwargs):
+ assert type(args[0]) is FunctionType # NOTE: The first one args is a function!
+ splitted_args, splitted_kwargs = _split_args_kwargs_data_proto(worker_group.world_size, *args[1:], **kwargs)
+ splitted_args_with_func = [[args[0]] * worker_group.world_size] + splitted_args
+ return splitted_args_with_func, splitted_kwargs
+
+
+def collect_dp_compute_data_proto(worker_group: "WorkerGroup", outputs: List[DataProto]) -> DataProto:
+ for output in outputs:
+ assert isinstance(output, (DataProto, ray.ObjectRef)), f"Expect a DataProto, but got {type(output)}"
+
+ outputs = collect_dp_compute(worker_group, outputs)
+ return _concat_data_proto_or_future(outputs)
+
+
+def get_predefined_dispatch_fn(dispatch_mode: Dispatch):
+ predefined_dispatch_mode_fn = {
+ Dispatch.ONE_TO_ALL: {
+ "dispatch_fn": dispatch_one_to_all,
+ "collect_fn": collect_all_to_all,
+ },
+ Dispatch.ALL_TO_ALL: {
+ "dispatch_fn": dispatch_all_to_all,
+ "collect_fn": collect_all_to_all,
+ },
+ Dispatch.DP_COMPUTE: {
+ "dispatch_fn": dispatch_dp_compute,
+ "collect_fn": collect_dp_compute,
+ },
+ Dispatch.DP_COMPUTE_PROTO: {
+ "dispatch_fn": dispatch_dp_compute_data_proto,
+ "collect_fn": collect_dp_compute_data_proto,
+ },
+ Dispatch.DP_COMPUTE_PROTO_WITH_FUNC: {
+ "dispatch_fn": dispatch_dp_compute_data_proto_with_func,
+ "collect_fn": collect_dp_compute_data_proto,
+ },
+ Dispatch.DP_COMPUTE_METRIC: {
+ "dispatch_fn": dispatch_dp_compute_data_proto,
+ "collect_fn": collect_dp_compute,
+ },
+ }
+ return predefined_dispatch_mode_fn[dispatch_mode]
+
+
+def get_predefined_execute_fn(execute_mode: Execute):
+ """
+ Note that here we only asks execute_all and execute_rank_zero to be implemented
+ Leave the choice of how these two functions handle argument 'blocking' to users
+ """
+ predefined_execute_mode_fn = {
+ Execute.ALL: {"execute_fn_name": "execute_all"},
+ Execute.RANK_ZERO: {"execute_fn_name": "execute_rank_zero"},
+ }
+ return predefined_execute_mode_fn[execute_mode]
+
+
+def _check_dispatch_mode(dispatch_mode: Union[Dispatch, Dict[Literal["dispatch_fn", "collect_fn"], FunctionType]]):
+ assert isinstance(dispatch_mode, (Dispatch, dict)), (
+ f"dispatch_mode must be a Dispatch or a Dict. Got {dispatch_mode}"
+ )
+ if isinstance(dispatch_mode, dict):
+ necessary_keys = ["dispatch_fn", "collect_fn"]
+ for key in necessary_keys:
+ assert key in dispatch_mode, f"key {key} should be in dispatch_mode if it is a dictionary"
+
+
+def _check_execute_mode(execute_mode: Execute):
+ assert isinstance(execute_mode, Execute), f"execute_mode must be a Execute. Got {execute_mode}"
+
+
+def _materialize_futures(*args, **kwargs):
+ new_args = []
+ for arg in args:
+ if isinstance(arg, DataProtoFuture):
+ arg = arg.get()
+ # add more type to materialize
+ new_args.append(arg)
+
+ for key, value in kwargs.items():
+ if isinstance(value, DataProtoFuture):
+ kwargs[key] = value.get()
+
+ new_args = tuple(new_args)
+ return new_args, kwargs
+
+
+def register(dispatch_mode=Dispatch.ALL_TO_ALL, execute_mode=Execute.ALL, blocking=True, materialize_futures=True):
+ _check_dispatch_mode(dispatch_mode=dispatch_mode)
+ _check_execute_mode(execute_mode=execute_mode)
+
+ def decorator(func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ if materialize_futures:
+ args, kwargs = _materialize_futures(*args, **kwargs)
+ return func(*args, **kwargs)
+
+ attrs = {"dispatch_mode": dispatch_mode, "execute_mode": execute_mode, "blocking": blocking}
+ setattr(inner, MAGIC_ATTR, attrs)
+ return inner
+
+ return decorator
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/ray.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/ray.py
new file mode 100644
index 0000000000000000000000000000000000000000..de7f702a894ef5cd87fa90931e6bd260f8b68b13
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/ray.py
@@ -0,0 +1,28 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import ray
+
+
+@ray.remote
+class WorkerGroupRegisterCenter:
+ def __init__(self, rank_zero_info):
+ self.rank_zero_info = rank_zero_info
+
+ def get_rank_zero_info(self):
+ return self.rank_zero_info
+
+
+def create_worker_group_register_center(name, info):
+ return WorkerGroupRegisterCenter.options(name=name).remote(info)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ecffca6b8d7589b292fbb25b5cf74c004cc4fad
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker.py
@@ -0,0 +1,202 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+the class for Worker
+"""
+
+import os
+import socket
+from dataclasses import dataclass
+from typing import Tuple
+
+import ray
+import torch
+
+from .decorator import Dispatch, Execute, register
+from .register_center.ray import create_worker_group_register_center
+
+
+@dataclass
+class DistRankInfo:
+ tp_rank: int
+ dp_rank: int
+ pp_rank: int
+
+
+@dataclass
+class DistGlobalInfo:
+ tp_size: int
+ dp_size: int
+ pp_size: int
+
+
+class WorkerHelper:
+ def _get_node_ip(self) -> str:
+ host_ipv4 = os.getenv("MY_HOST_IP", None)
+ host_ipv6 = os.getenv("MY_HOST_IPV6", None)
+ host_ip_by_env = host_ipv4 or host_ipv6
+ host_ip_by_sdk = ray._private.services.get_node_ip_address()
+
+ host_ip = host_ip_by_env or host_ip_by_sdk
+ return host_ip
+
+ def _get_free_port(self) -> int:
+ with socket.socket() as sock:
+ sock.bind(("", 0))
+ return sock.getsockname()[1]
+
+ def get_availale_master_addr_port(self) -> Tuple[str, str]:
+ return self._get_node_ip(), str(self._get_free_port())
+
+ def _get_pid(self):
+ return
+
+
+class WorkerMeta:
+ keys = [
+ "WORLD_SIZE",
+ "RANK",
+ "LOCAL_WORLD_SIZE",
+ "LOCAL_RANK",
+ "MASTER_ADDR",
+ "MASTER_PORT",
+ "CUDA_VISIBLE_DEVICES",
+ ]
+
+ def __init__(self, store) -> None:
+ self._store = store
+
+ def to_dict(self):
+ return {f"_{key.lower()}": self._store.get(f"_{key.lower()}", None) for key in WorkerMeta.keys}
+
+
+# we assume that in each WorkerGroup, there is a Master Worker
+class Worker(WorkerHelper):
+ """A (distributed) worker."""
+
+ _world_size: int
+ _rank: int
+ _local_world_size: int
+ _local_rank: int
+ _master_addr: str
+ _master_port: str
+ _cuda_visible_devices: str
+
+ def __new__(cls, *args, **kwargs):
+ instance = super().__new__(cls)
+
+ # note that here we use int to distinguish
+ disable_worker_init = int(os.getenv("DISABLE_WORKER_INIT", 0))
+ if disable_worker_init:
+ return instance
+
+ rank = os.getenv("RANK", None)
+ worker_group_prefix = os.getenv("WG_PREFIX", None)
+
+ # when decorator @ray.remote applies, __new__ will be called while we don't want to apply _configure_before_init
+ if None not in [rank, worker_group_prefix] and "ActorClass(" not in cls.__name__:
+ instance._configure_before_init(f"{worker_group_prefix}_register_center", int(rank))
+
+ return instance
+
+ def _configure_before_init(self, register_center_name: str, rank: int):
+ assert isinstance(rank, int), f"rank must be int, instead of {type(rank)}"
+
+ if rank == 0:
+ master_addr, master_port = self.get_availale_master_addr_port()
+ rank_zero_info = {
+ "MASTER_ADDR": master_addr,
+ "MASTER_PORT": master_port,
+ }
+ self.register_center = create_worker_group_register_center(name=register_center_name, info=rank_zero_info)
+ os.environ.update(rank_zero_info)
+
+ def __init__(self, cuda_visible_devices=None) -> None:
+ # construct a meta from envrionment variable. Note that the import must be inside the class because it is executed remotely
+ world_size = int(os.getenv("WORLD_SIZE"))
+ rank = int(os.getenv("RANK"))
+ self._rank = rank
+ self._world_size = world_size
+
+ if "AMD" in torch.cuda.get_device_name():
+ os.environ["CUDA_VISIBLE_DEVICES"] = os.getenv("ROCR_VISIBLE_DEVICES")
+ os.environ["LOCAL_RANK"] = os.getenv("RAY_LOCAL_RANK")
+ cuda_visible_devices = os.getenv("LOCAL_RANK", "0")
+ torch.cuda.set_device(int(cuda_visible_devices))
+
+ master_addr = os.getenv("MASTER_ADDR")
+ master_port = os.getenv("MASTER_PORT")
+
+ local_world_size = int(os.getenv("LOCAL_WORLD_SIZE", "1"))
+ local_rank = int(os.getenv("LOCAL_RANK", "0"))
+
+ store = {
+ "_world_size": world_size,
+ "_rank": rank,
+ "_local_world_size": local_world_size,
+ "_local_rank": local_rank,
+ "_master_addr": master_addr,
+ "_master_port": master_port,
+ }
+ if cuda_visible_devices is not None:
+ store["_cuda_visible_devices"] = cuda_visible_devices
+
+ meta = WorkerMeta(store=store)
+ self._configure_with_meta(meta=meta)
+
+ def _configure_with_meta(self, meta: WorkerMeta):
+ """
+ This function should only be called inside by WorkerGroup
+ """
+ assert isinstance(meta, WorkerMeta)
+ self.__dict__.update(meta.to_dict()) # this is hacky
+ # print(f"__dict__: {self.__dict__}")
+ for key in WorkerMeta.keys:
+ val = self.__dict__.get(f"_{key.lower()}", None)
+ if val is not None:
+ # print(f"set {key} to {val}")
+ os.environ[key] = str(val)
+
+ os.environ["REDIS_STORE_SERVER_HOST"] = (
+ str(self._master_addr).replace("[", "").replace("]", "") if self._master_addr else ""
+ )
+
+ def get_master_addr_port(self):
+ return self._master_addr, self._master_port
+
+ def get_cuda_visible_devices(self):
+ cuda_visible_devices = os.getenv("CUDA_VISIBLE_DEVICES", "not set")
+ return cuda_visible_devices
+
+ def print_rank0(self, *args, **kwargs):
+ if self.rank == 0:
+ print(*args, **kwargs)
+
+ @property
+ def world_size(self):
+ return self._world_size
+
+ @property
+ def rank(self):
+ return self._rank
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO_WITH_FUNC)
+ def execute_with_func_generator(self, func, *args, **kwargs):
+ ret_proto = func(self, *args, **kwargs)
+ return ret_proto
+
+ @register(dispatch_mode=Dispatch.ALL_TO_ALL, execute_mode=Execute.RANK_ZERO)
+ def execute_func_rank_zero(self, func, *args, **kwargs):
+ result = func(*args, **kwargs)
+ return result
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker_group.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker_group.py
new file mode 100644
index 0000000000000000000000000000000000000000..8648fbf1b81cf165af3547eefeb43d9bd40d3c28
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker_group.py
@@ -0,0 +1,194 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+the class of WorkerGroup
+"""
+
+import logging
+import signal
+import threading
+import time
+from typing import Any, Callable, Dict, List, Optional
+
+from .decorator import MAGIC_ATTR, Dispatch, get_predefined_dispatch_fn, get_predefined_execute_fn
+
+
+class ResourcePool:
+ """The resource pool with meta info such as world size."""
+
+ def __init__(
+ self, process_on_nodes: Optional[Any] = None, max_colocate_count: int = 10, n_gpus_per_node: int = 8
+ ) -> None:
+ if process_on_nodes is None:
+ process_on_nodes = []
+
+ self._store = process_on_nodes
+ self.max_colocate_count = max_colocate_count
+ self.n_gpus_per_node = n_gpus_per_node # this is left for future huawei GPU that contains 16 GPUs per node
+
+ def add_node(self, process_count):
+ self._store.append(process_count)
+
+ @property
+ def world_size(self):
+ return sum(self._store)
+
+ def __call__(self) -> Any:
+ return self._store
+
+ @property
+ def store(self):
+ return self._store
+
+ def local_world_size_list(self) -> List[int]:
+ nested_local_world_size_list = [
+ [local_world_size for _ in range(local_world_size)] for local_world_size in self._store
+ ]
+ return [item for row in nested_local_world_size_list for item in row]
+
+ def local_rank_list(self) -> List[int]:
+ nested_local_rank_list = [[i for i in range(local_world_size)] for local_world_size in self._store] # noqa: C416
+ return [item for row in nested_local_rank_list for item in row]
+
+
+class ClassWithInitArgs:
+ """
+ This class stores a class constructor and the args/kwargs to construct the class.
+ It is used to instantiate the remote class.
+ """
+
+ def __init__(self, cls, *args, **kwargs) -> None:
+ self.cls = cls
+ self.args = args
+ self.kwargs = kwargs
+
+ def __call__(self) -> Any:
+ return self.cls(*self.args, **self.kwargs)
+
+
+def check_workers_alive(workers: List, is_alive: Callable, gap_time: float = 1) -> None:
+ while True:
+ for worker in workers:
+ if not is_alive(worker):
+ logging.warning(f"Worker {worker} is not alive, sending signal to main thread")
+ signal.raise_signal(signal.SIGABRT)
+
+ time.sleep(gap_time)
+
+
+class WorkerGroup:
+ """A group of workers"""
+
+ def __init__(self, resource_pool: ResourcePool, **kwargs) -> None:
+ self._is_init_with_detached_workers = True if resource_pool is None else False
+
+ if resource_pool is not None:
+ # handle the case when WorkGroup is attached to an existing one
+ self._procecss_dispatch_config = resource_pool()
+ else:
+ self._procecss_dispatch_config = None
+
+ self._workers = []
+ self._worker_names = []
+
+ self._master_addr = None
+ self._master_port = None
+
+ self._checker_thread: threading.Thread = None
+
+ def _is_worker_alive(self, worker):
+ raise NotImplementedError("WorkerGroup._is_worker_alive called, should be implemented in derived class.")
+
+ def _block_until_all_workers_alive(self) -> None:
+ while True:
+ all_state = [self._is_worker_alive(worker) for worker in self._workers]
+ if False in all_state:
+ time.sleep(1)
+ else:
+ break
+
+ def start_worker_aliveness_check(self, every_n_seconds=1) -> None:
+ # before starting checking worker aliveness, make sure all workers are already alive
+ self._block_until_all_workers_alive()
+
+ self._checker_thread = threading.Thread(
+ target=check_workers_alive, args=(self._workers, self._is_worker_alive, every_n_seconds)
+ )
+ self._checker_thread.start()
+
+ @property
+ def world_size(self):
+ return len(self._workers)
+
+ def _bind_worker_method(self, user_defined_cls, func_generator):
+ """
+ Bind the worker method to the WorkerGroup
+ """
+ for method_name in dir(user_defined_cls):
+ try:
+ method = getattr(user_defined_cls, method_name)
+ assert callable(method), f"{method_name} in {user_defined_cls} is not callable"
+ except Exception:
+ # if it is a property, it will fail because Class doesn't have instance property
+ continue
+
+ if hasattr(method, MAGIC_ATTR):
+ # this method is decorated by register
+ attribute = getattr(method, MAGIC_ATTR)
+ assert isinstance(attribute, Dict), f"attribute must be a dictionary. Got {type(attribute)}"
+ assert "dispatch_mode" in attribute, "attribute must contain dispatch_mode in its key"
+
+ dispatch_mode = attribute["dispatch_mode"]
+ execute_mode = attribute["execute_mode"]
+ blocking = attribute["blocking"]
+
+ # get dispatch fn
+ if isinstance(dispatch_mode, Dispatch):
+ # get default dispatch fn
+ fn = get_predefined_dispatch_fn(dispatch_mode=dispatch_mode)
+ dispatch_fn = fn["dispatch_fn"]
+ collect_fn = fn["collect_fn"]
+ else:
+ assert isinstance(dispatch_mode, dict)
+ assert "dispatch_fn" in dispatch_mode
+ assert "collect_fn" in dispatch_mode
+ dispatch_fn = dispatch_mode["dispatch_fn"]
+ collect_fn = dispatch_mode["collect_fn"]
+
+ # get execute_fn_name
+ execute_mode = get_predefined_execute_fn(execute_mode=execute_mode)
+ wg_execute_fn_name = execute_mode["execute_fn_name"]
+
+ # get execute_fn from string
+ try:
+ execute_fn = getattr(self, wg_execute_fn_name)
+ assert callable(execute_fn), "execute_fn must be callable"
+ except Exception:
+ print(f"execute_fn {wg_execute_fn_name} is invalid")
+ raise
+
+ # bind a new method to the RayWorkerGroup
+ func = func_generator(
+ self,
+ method_name,
+ dispatch_fn=dispatch_fn,
+ collect_fn=collect_fn,
+ execute_fn=execute_fn,
+ blocking=blocking,
+ )
+
+ try:
+ setattr(self, method_name, func)
+ except Exception:
+ raise ValueError(f"Fail to set method_name {method_name}")
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..25b3141cee9f146d18c92b5625a327a1d9f2dbce
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .base import RayClassWithInitArgs, RayResourcePool, RayWorkerGroup, create_colocated_worker_cls
+
+
+__all__ = ["RayClassWithInitArgs", "RayResourcePool", "RayWorkerGroup", "create_colocated_worker_cls"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/base.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..98273125f79df6fb215ab8b63f855423ff44c40a
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/base.py
@@ -0,0 +1,493 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import random
+import re
+import string
+import time
+from typing import Any, Dict, List, Optional, Tuple
+from unittest.mock import patch
+
+import ray
+from ray.actor import ActorHandle
+from ray.experimental.state.api import get_actor
+from ray.util import list_named_actors
+from ray.util.placement_group import PlacementGroup, placement_group
+from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy, PlacementGroupSchedulingStrategy
+
+from ..base import ClassWithInitArgs, ResourcePool, Worker, WorkerGroup
+from ..base.decorator import MAGIC_ATTR
+
+
+__all__ = ["Worker"]
+
+
+def get_random_string(length: int) -> str:
+ letters_digits = string.ascii_letters + string.digits
+ return "".join(random.choice(letters_digits) for _ in range(length))
+
+
+def func_generator(self, method_name, dispatch_fn, collect_fn, execute_fn, blocking):
+ def func(*args, **kwargs):
+ args, kwargs = dispatch_fn(self, *args, **kwargs)
+ output = execute_fn(method_name, *args, **kwargs)
+ if blocking:
+ output = ray.get(output)
+ output = collect_fn(self, output)
+ return output
+
+ return func
+
+
+def sort_placement_group_by_node_ip(pgs: List[PlacementGroup]) -> List[PlacementGroup]:
+ """
+ Sort the placement groups by node ip, all bundles in a single placement group should be on the same node.
+
+ FSDPCheckpointManager saves sharded model states and optimizer states in local storage, which requires RANK
+ to be consistent across nodes when resume from checkpoint.
+
+ With this function, if there's only one resource pool and there's no node change, RANK should be consistent
+ across nodes in multiple ray jobs, even if the whole ray cluster is restarted.
+ """
+ node_ip = {node["NodeID"]: node["NodeManagerAddress"] for node in ray.nodes()}
+ pg_ip = {}
+ for pg in pgs:
+ specs = ray._private.state.state.placement_group_table(pg.id)
+ # all bunles should be on the same node
+ node_id = specs["bundles_to_node_id"][0]
+ pg_ip[pg.id] = node_ip[node_id]
+
+ return sorted(pgs, key=lambda pg: pg_ip[pg.id])
+
+
+class RayResourcePool(ResourcePool):
+ def __init__(
+ self,
+ process_on_nodes: List[int] = None,
+ use_gpu: bool = True,
+ name_prefix: str = "",
+ max_colocate_count: int = 5,
+ detached: bool = False,
+ ) -> None:
+ super().__init__(process_on_nodes, max_colocate_count)
+ self.use_gpu = use_gpu
+ # print(f"in RayProcessDispatchConfiguration: name_prefix = {name_prefix}")
+ self.name_prefix = name_prefix
+ self.pgs = None
+ self.detached = detached
+
+ def get_placement_groups(self, strategy: str = "STRICT_PACK", name: Optional[str] = None) -> List[PlacementGroup]:
+ if self.pgs is not None:
+ return self.pgs
+
+ pg_name_prefix = (
+ name if name else f"{self.name_prefix}verl_group_{'_'.join([str(count) for count in self._store])}:"
+ )
+ # print(f"pg_name_prefix = {pg_name_prefix}")
+ pg_scheme = [
+ [
+ {"CPU": self.max_colocate_count, "GPU": 1} if self.use_gpu else {"CPU": self.max_colocate_count}
+ for _ in range(process_count)
+ ]
+ for process_count in self._store
+ ]
+
+ lifetime = "detached" if self.detached else None
+
+ pgs = [
+ placement_group(bundles=bundles, strategy=strategy, name=pg_name_prefix + str(idx), lifetime=lifetime)
+ for idx, bundles in enumerate(pg_scheme)
+ ]
+
+ ray.get([pg.ready() for pg in pgs])
+
+ self.pgs = pgs
+ return pgs
+
+
+def extract_pg_from_exist(
+ resource_pools: Dict[str, RayResourcePool], src_role_names: List[str], resource_pool: RayResourcePool
+) -> List[PlacementGroup]:
+ src_pgs = [
+ pg
+ for role_name, resource_pool in resource_pools.items()
+ for pg in resource_pool.get_placement_groups()
+ if role_name in src_role_names
+ ]
+
+ sorted_src_pgs = sorted(src_pgs, key=lambda pg: pg.bundle_count, reverse=True)
+ sorted_process_on_nodes = sorted([(val, idx) for idx, val in enumerate(resource_pool.store)], reverse=True)
+
+ unsorted_pgs: List[Tuple[int, PlacementGroup]] = []
+ searching_idx = 0
+ for request_process, original_idx in sorted_process_on_nodes:
+ assert searching_idx < len(sorted_src_pgs), f"no enough nodes for request: searching {searching_idx} th node"
+ assert request_process <= sorted_src_pgs[searching_idx].bundle_count, (
+ f"requesting {request_process} processes, bundle count cannot satisfy"
+ )
+ unsorted_pgs.append((original_idx, sorted_src_pgs[searching_idx]))
+ searching_idx += 1
+
+ return [pg for _, pg in sorted(unsorted_pgs)]
+
+
+def merge_resource_pool(rp1: RayResourcePool, rp2: RayResourcePool) -> RayResourcePool:
+ assert rp1.use_gpu == rp2.use_gpu, "Both RayResourcePool must either use_gpu or not"
+ assert rp1.max_colocate_count == rp2.max_colocate_count, (
+ "Both RayResourcePool must has the same max_colocate_count"
+ )
+ assert rp1.n_gpus_per_node == rp2.n_gpus_per_node, "Both RayResourcePool must has the same n_gpus_per_node"
+ assert rp1.detached == rp2.detached, "Detached ResourcePool cannot be merged with non-detached ResourcePool"
+
+ new_store = rp1.store + rp2.store
+
+ merged = RayResourcePool(new_store, rp1.use_gpu, f"{rp1.name_prefix}_{rp2.name_prefix}")
+ merged.pgs = rp1.get_placement_groups() + rp2.get_placement_groups()
+
+ return merged
+
+
+class RayClassWithInitArgs(ClassWithInitArgs):
+ def __init__(self, cls, *args, **kwargs) -> None:
+ # self._options = kwargs.pop('options', dict())
+ super().__init__(cls, *args, **kwargs)
+ self._options = {}
+ self._additional_resource = {}
+
+ def set_additional_resource(self, additional_resource):
+ self._additional_resource = additional_resource
+
+ def update_options(self, options: Dict):
+ self._options.update(options)
+
+ def __call__(
+ self,
+ placement_group: PlacementGroup,
+ placement_group_bundle_idx: int,
+ use_gpu: bool = True,
+ num_gpus: int = 1,
+ sharing_with: Worker = None,
+ ) -> Any:
+ if sharing_with is not None:
+ target_node_id = ray.get(sharing_with.get_node_id.remote())
+ cuda_visible_devices = ray.get(sharing_with.get_cuda_visible_devices.remote())
+ options = {"scheduling_strategy": NodeAffinitySchedulingStrategy(node_id=target_node_id, soft=False)}
+ return self.cls.options(**options).remote(
+ *self.args, cuda_visible_devices=cuda_visible_devices, **self.kwargs
+ )
+
+ options = {
+ "scheduling_strategy": PlacementGroupSchedulingStrategy(
+ placement_group=placement_group, placement_group_bundle_index=placement_group_bundle_idx
+ )
+ }
+ options.update(self._options)
+
+ if use_gpu:
+ options["num_gpus"] = num_gpus
+
+ if len(self._additional_resource) > 1:
+ for k, v in self._additional_resource.items():
+ options[k] = v
+
+ # print("cls:", self.cls)
+ # print("args: ", self.args)
+ # print("kwargs: ", self.kwargs)
+ return self.cls.options(**options).remote(*self.args, **self.kwargs)
+
+
+class RayWorkerGroup(WorkerGroup):
+ def __init__(
+ self,
+ resource_pool: RayResourcePool = None,
+ ray_cls_with_init: RayClassWithInitArgs = None,
+ bin_pack: bool = True,
+ name_prefix: str = None,
+ detached: bool = False,
+ worker_names: List[str] = None,
+ **kwargs,
+ ) -> None:
+ super().__init__(resource_pool=resource_pool, **kwargs)
+ self.ray_cls_with_init = ray_cls_with_init
+ self.name_prefix = get_random_string(length=6) if name_prefix is None else name_prefix
+
+ if worker_names is not None:
+ assert self._is_init_with_detached_workers
+ self._worker_names = worker_names
+
+ if self._is_init_with_detached_workers:
+ self._init_with_detached_workers(worker_names=worker_names)
+ else:
+ self._init_with_resource_pool(
+ resource_pool=resource_pool, ray_cls_with_init=ray_cls_with_init, bin_pack=bin_pack, detached=detached
+ )
+
+ if ray_cls_with_init is not None:
+ self._bind_worker_method(self.ray_cls_with_init.cls, func_generator)
+
+ def _is_worker_alive(self, worker: ActorHandle) -> bool:
+ worker_state_dict = get_actor(worker._actor_id.hex())
+ return worker_state_dict.get("state", "undefined") == "ALIVE" if worker_state_dict is not None else False
+
+ def _init_with_detached_workers(self, worker_names: List[str]) -> None:
+ workers = [ray.get_actor(name=name) for name in worker_names]
+ self._workers = workers
+ self._world_size = len(worker_names)
+
+ def _init_with_resource_pool(
+ self, resource_pool: RayResourcePool, ray_cls_with_init: RayClassWithInitArgs, bin_pack: bool, detached: bool
+ ):
+ use_gpu = resource_pool.use_gpu
+
+ strategy = "PACK"
+ if bin_pack:
+ strategy = "STRICT_PACK"
+
+ pgs = resource_pool.get_placement_groups(strategy=strategy)
+ world_size = resource_pool.world_size
+ self._world_size = world_size
+ # cia.add_kwarg("_world_size", world_size)
+ num_gpus = 1 / resource_pool.max_colocate_count
+
+ rank = -1
+ local_world_size = resource_pool.store[0]
+ for pg_idx, pg in enumerate(sort_placement_group_by_node_ip(pgs)):
+ assert local_world_size <= pg.bundle_count, f"when generating for {self.name_prefix}, for the "
+ for local_rank in range(local_world_size):
+ rank += 1
+
+ # we pass in environment variable at option so that Worker can use environment variable to set
+ env_vars = {
+ "WORLD_SIZE": str(world_size),
+ "RANK": str(rank),
+ "WG_PREFIX": self.name_prefix,
+ "WG_BACKEND": "ray",
+ "RAY_LOCAL_WORLD_SIZE": str(local_world_size),
+ "RAY_LOCAL_RANK": str(local_rank),
+ }
+ if rank != 0:
+ env_vars["MASTER_ADDR"] = self._master_addr
+ env_vars["MASTER_PORT"] = self._master_port
+
+ cia_name = type(ray_cls_with_init.cls).__name__
+ match = re.search(r"ActorClass\(([^)]+)\)", cia_name) # ray.remote(Obj) -> "ActorClass(Obj)"
+ cia_name = match.group(1) if match else cia_name # "ActorClass(Obj)" -> "Obj"
+ name = f"{self.name_prefix}{cia_name}_{pg_idx}:{local_rank}" # e.g. Worker_2:5
+
+ ray_cls_with_init.update_options({"runtime_env": {"env_vars": env_vars}, "name": name})
+
+ if detached:
+ ray_cls_with_init.update_options({"lifetime": "detached"})
+
+ # create a worker
+ worker = ray_cls_with_init(
+ placement_group=pg, placement_group_bundle_idx=local_rank, use_gpu=use_gpu, num_gpus=num_gpus
+ )
+ self._workers.append(worker)
+ self._worker_names.append(name)
+
+ if rank == 0:
+ register_center_actor = None
+ for _ in range(120):
+ if f"{self.name_prefix}_register_center" not in list_named_actors():
+ time.sleep(1)
+ else:
+ register_center_actor = ray.get_actor(f"{self.name_prefix}_register_center")
+ break
+ assert register_center_actor is not None, (
+ f"failed to get register_center_actor: {self.name_prefix}_register_center in {list_named_actors(all_namespaces=True)}"
+ )
+ rank_zero_info = ray.get(register_center_actor.get_rank_zero_info.remote())
+ self._master_addr, self._master_port = rank_zero_info["MASTER_ADDR"], rank_zero_info["MASTER_PORT"]
+ # print(f"rank_zero_info: {rank_zero_info}")
+ # print(f"master_addr: {self._master_addr}, master_port: {self._master_port}")
+
+ @property
+ def worker_names(self):
+ return self._worker_names
+
+ @classmethod
+ def from_detached(cls, worker_names=None, ray_cls_with_init=None):
+ worker_group = cls(
+ resource_pool=None, ray_cls_with_init=ray_cls_with_init, name_prefix=None, worker_names=worker_names
+ )
+ return worker_group
+
+ def spawn(self, prefix_set):
+ """
+ spawn to a dictionary of worker groups, each with a subset of method with prefix.
+
+ """
+
+ def _rebind_actor_methods(worker_group, actor_name):
+ """
+ bind the method with actor_prefix to its original name
+ """
+ prefix: str = actor_name + "_"
+ for method_name in dir(worker_group):
+ if method_name.startswith(prefix):
+ # only valid when Python >= 3.9
+ original_method_name = method_name.removeprefix(prefix)
+ method = getattr(worker_group, method_name)
+ setattr(worker_group, original_method_name, method)
+
+ new_worker_group_dict = {}
+ for prefix in prefix_set:
+ new_worker_group = self.from_detached(
+ worker_names=self._worker_names, ray_cls_with_init=self.ray_cls_with_init
+ )
+
+ _rebind_actor_methods(new_worker_group, prefix)
+ new_worker_group_dict[prefix] = new_worker_group
+ return new_worker_group_dict
+
+ def execute_rank_zero_sync(self, method_name: str, *args, **kwargs):
+ return ray.get(self.execute_rank_zero_async(method_name, *args, **kwargs))
+
+ def execute_rank_zero_async(self, method_name: str, *args, **kwargs):
+ remote_call = getattr(self._workers[0], method_name)
+ return remote_call.remote(*args, **kwargs)
+
+ def execute_rank_zero(self, method_name: str, *args, **kwargs):
+ return self.execute_rank_zero_async(method_name, *args, **kwargs)
+
+ def execute_all(self, method_name: str, *args, **kwargs):
+ return self.execute_all_async(method_name, *args, **kwargs)
+
+ def execute_all_sync(self, method_name: str, *args, **kwargs):
+ return ray.get(self.execute_all_async(method_name, *args, **kwargs))
+
+ def execute_all_async(self, method_name: str, *args, **kwargs):
+ # Here we assume that if all the parameters in args and kwargs are lists,
+ # and the lengths of all these lists are the same as len(self._workers),
+ # then we will send each element in the list to the corresponding worker.
+ # print(f"execute_all_async: method {method_name}({args}, {kwargs})")
+ length = len(self._workers)
+ if all(isinstance(arg, list) for arg in args) and all(isinstance(kwarg, list) for kwarg in kwargs.values()):
+ if all(len(arg) == length for arg in args) and all(len(kwarg) == length for kwarg in kwargs.values()):
+ # print(f"splitting args and kwargs into {length} shards")
+ result = []
+ for i in range(length):
+ sliced_args = tuple(arg[i] for arg in args)
+ sliced_kwargs = {k: v[i] for k, v in kwargs.items()}
+ remote_call = getattr(self._workers[i], method_name)
+ result.append(remote_call.remote(*sliced_args, **sliced_kwargs))
+ return result
+
+ return [getattr(worker, method_name).remote(*args, **kwargs) for worker in self._workers]
+
+ @property
+ def master_address(self):
+ return self._master_addr
+
+ @property
+ def master_port(self):
+ return self._master_port
+
+ @property
+ def workers(self):
+ return self._workers
+
+ @property
+ def world_size(self):
+ return self._world_size
+
+
+"""
+Utilities that enables creating workers inside the same ray.Actor,
+with code written in separate ray.Actors.
+"""
+
+
+def _bind_workers_method_to_parent(cls, key, user_defined_cls):
+ """
+ Binds the methods of each worker to the WorkerDict.
+ Note that we only bind public methods that are decorated by register
+ """
+ for method_name in dir(user_defined_cls):
+ try:
+ method = getattr(user_defined_cls, method_name)
+ assert callable(method), f"{method_name} in {user_defined_cls} is not callable"
+ except Exception:
+ # if it is a property, it will fail because Class doesn't have instance property
+ continue
+
+ if hasattr(method, MAGIC_ATTR):
+
+ def generate_function(name):
+ def func(self, *args, **kwargs):
+ # dispatch to the actual worker
+ return getattr(self.worker_dict[key], name)(*args, **kwargs)
+
+ return func
+
+ func = generate_function(method_name)
+ # pass MAGIC_ATTR for outer worker group
+ setattr(func, MAGIC_ATTR, getattr(method, MAGIC_ATTR))
+ try:
+ method_name_with_prefix = key + "_" + method_name
+ setattr(cls, method_name_with_prefix, func)
+ # print(f'Binding {method_name_with_prefix}')
+ except Exception:
+ raise ValueError(f"Fail to set method_name {method_name}")
+
+
+def _unwrap_ray_remote(cls):
+ if hasattr(cls, "__ray_actor_class__"):
+ cls = cls.__ray_actor_class__
+ return cls
+
+
+def create_colocated_worker_cls(class_dict: dict[str, RayClassWithInitArgs]):
+ """
+ This function should return a class instance that delegates the calls to every
+ cls in cls_dict
+ """
+ cls_dict = {}
+ init_args_dict = {}
+ worker_cls = None
+ for key, cls in class_dict.items():
+ if worker_cls is None:
+ worker_cls = cls.cls.__ray_actor_class__.__base__
+ else:
+ assert worker_cls == cls.cls.__ray_actor_class__.__base__, (
+ "the worker class should be the same when share the same process"
+ )
+ cls_dict[key] = cls.cls
+ init_args_dict[key] = {"args": cls.args, "kwargs": cls.kwargs}
+
+ assert cls_dict.keys() == init_args_dict.keys()
+
+ # TODO: create a class with customizable name
+ class WorkerDict(worker_cls):
+ def __init__(self):
+ super().__init__()
+ self.worker_dict = {}
+ for key, user_defined_cls in cls_dict.items():
+ user_defined_cls = _unwrap_ray_remote(user_defined_cls)
+ # directly instantiate the class without remote
+ with patch.dict(os.environ, {"DISABLE_WORKER_INIT": "1"}):
+ self.worker_dict[key] = user_defined_cls(
+ *init_args_dict[key].get("args", ()), **init_args_dict[key].get("kwargs", {})
+ )
+
+ # now monkey-patch the methods from inner class to WorkerDict
+ for key, user_defined_cls in cls_dict.items():
+ user_defined_cls = _unwrap_ray_remote(user_defined_cls)
+ _bind_workers_method_to_parent(WorkerDict, key, user_defined_cls)
+
+ remote_cls = ray.remote(WorkerDict)
+ remote_cls = RayClassWithInitArgs(cls=remote_cls)
+ return remote_cls
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/config.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1faad80632b9ff5ce6e2c958e9447d2e392ba31
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/config.py
@@ -0,0 +1,123 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+PPO config
+"""
+
+import os
+from dataclasses import asdict, dataclass, field, fields, is_dataclass
+from typing import Optional, Tuple
+
+from ..workers.config import WorkerConfig
+
+
+def recursive_post_init(dataclass_obj):
+ if hasattr(dataclass_obj, "post_init"):
+ dataclass_obj.post_init()
+
+ for attr in fields(dataclass_obj):
+ if is_dataclass(getattr(dataclass_obj, attr.name)):
+ recursive_post_init(getattr(dataclass_obj, attr.name))
+
+
+@dataclass
+class DataConfig:
+ train_files: str = ""
+ val_files: str = ""
+ prompt_key: str = "prompt"
+ answer_key: str = "answer"
+ image_key: str = "images"
+ max_prompt_length: int = 512
+ max_response_length: int = 512
+ rollout_batch_size: int = 512
+ val_batch_size: int = -1
+ format_prompt: Optional[str] = None
+ override_chat_template: Optional[str] = None
+ shuffle: bool = True
+ seed: int = 1
+ max_pixels: int = 4194304
+ min_pixels: int = 262144
+ filter_overlong_prompts: bool = True
+
+ def post_init(self):
+ if self.format_prompt is not None:
+ if os.path.exists(self.format_prompt): # ray job uses absolute path
+ self.format_prompt = os.path.abspath(self.format_prompt)
+ else:
+ self.format_prompt = None
+
+
+@dataclass
+class AlgorithmConfig:
+ gamma: float = 1.0
+ lam: float = 1.0
+ adv_estimator: str = "grpo"
+ disable_kl: bool = False
+ use_kl_loss: bool = False
+ kl_penalty: str = "kl"
+ kl_coef: float = 1e-3
+ kl_type: str = "fixed"
+ kl_horizon: float = 0.0
+ kl_target: float = 0.0
+
+
+@dataclass
+class TrainerConfig:
+ total_epochs: int = 10
+ max_steps: Optional[int] = None
+ project_name: str = "easy_r1"
+ experiment_name: str = "demo"
+ logger: Tuple[str] = ("console", "wandb")
+ nnodes: int = 1
+ n_gpus_per_node: int = 8
+ critic_warmup: int = 0
+ val_freq: int = -1
+ val_before_train: bool = True
+ val_only: bool = False
+ val_generations_to_log: int = 0
+ save_freq: int = -1
+ save_limit: int = -1
+ save_checkpoint_path: Optional[str] = None
+ load_checkpoint_path: Optional[str] = None
+
+ def post_init(self):
+ if self.save_checkpoint_path is None:
+ self.save_checkpoint_path = os.path.join("checkpoints", self.project_name, self.experiment_name)
+
+ self.save_checkpoint_path = os.path.abspath(self.save_checkpoint_path) # ray job uses absolute path
+ if self.load_checkpoint_path is not None:
+ self.load_checkpoint_path = os.path.abspath(self.load_checkpoint_path)
+
+
+@dataclass
+class PPOConfig:
+ data: DataConfig = field(default_factory=DataConfig)
+ worker: WorkerConfig = field(default_factory=WorkerConfig)
+ algorithm: AlgorithmConfig = field(default_factory=AlgorithmConfig)
+ trainer: TrainerConfig = field(default_factory=TrainerConfig)
+
+ def post_init(self):
+ self.worker.rollout.prompt_length = self.data.max_prompt_length
+ self.worker.rollout.response_length = self.data.max_response_length
+ self.worker.rollout.trust_remote_code = self.worker.actor.model.trust_remote_code
+ self.worker.actor.disable_kl = self.algorithm.disable_kl
+ self.worker.actor.use_kl_loss = self.algorithm.use_kl_loss
+ self.worker.actor.kl_penalty = self.algorithm.kl_penalty
+ self.worker.actor.kl_coef = self.algorithm.kl_coef
+
+ def deep_post_init(self):
+ recursive_post_init(self)
+
+ def to_dict(self):
+ return asdict(self)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/core_algos.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/core_algos.py
new file mode 100644
index 0000000000000000000000000000000000000000..86f9410496676633c11b73f17880e7640d3b5b4c
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/core_algos.py
@@ -0,0 +1,428 @@
+# Copyright 2022 The HuggingFace Team
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Core functions to implement PPO algorithms.
+The function implemented in this file should be used by trainer with different distributed strategies to
+implement PPO
+"""
+
+from abc import ABC, abstractmethod
+from collections import defaultdict
+from typing import TYPE_CHECKING, Tuple
+
+import numpy as np
+import torch
+import torch.nn.functional as F
+
+from ..utils import torch_functional as VF
+
+
+if TYPE_CHECKING:
+ from .config import AlgorithmConfig
+
+
+class KLController(ABC):
+ kl_coef: float
+ """KL coefficient."""
+
+ @abstractmethod
+ def update(self, current_kl: float, n_steps: int) -> None:
+ """Update kl_coef according to current KL."""
+ ...
+
+
+class AdaptiveKLController(KLController):
+ """Adaptive KL controller described in: https://arxiv.org/pdf/1909.08593.pdf
+
+ Copied from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/utils.py#L54"""
+
+ def __init__(self, init_kl_coef: float, target_kl: float, horizon: float):
+ self.kl_coef = init_kl_coef
+ self.target = target_kl
+ self.horizon = horizon
+
+ def update(self, current_kl: float, n_steps: int) -> None:
+ target = self.target
+ proportional_error = np.clip(current_kl / target - 1, -0.2, 0.2)
+ mult = 1 + proportional_error * n_steps / self.horizon
+ self.kl_coef *= mult
+
+
+class FixedKLController(KLController):
+ """Fixed KL controller.
+
+ Copeid from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/utils.py#L72"""
+
+ def __init__(self, init_kl_coef: float):
+ self.kl_coef = init_kl_coef
+
+ def update(self, current_kl: float, n_steps: int) -> None:
+ pass
+
+
+def get_kl_controller(algorithm_config: "AlgorithmConfig") -> KLController:
+ """Adapted from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L319"""
+ if algorithm_config.kl_type == "fixed":
+ kl_ctrl = FixedKLController(init_kl_coef=algorithm_config.kl_coef)
+ elif algorithm_config.kl_type == "adaptive":
+ assert algorithm_config.kl_horizon > 0, f"horizon must be larger than 0. Got {algorithm_config.kl_horizon}."
+ kl_ctrl = AdaptiveKLController(
+ init_kl_coef=algorithm_config.kl_coef,
+ target_kl=algorithm_config.kl_target,
+ horizon=algorithm_config.kl_horizon,
+ )
+ else:
+ raise ValueError(f"Unknown kl type: {algorithm_config.kl_type}.")
+
+ return kl_ctrl
+
+
+@torch.no_grad()
+def compute_gae_advantage_return(
+ token_level_rewards: torch.Tensor,
+ values: torch.Tensor,
+ response_mask: torch.Tensor,
+ gamma: torch.Tensor,
+ lam: torch.Tensor,
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Adapted from https://github.com/huggingface/trl/blob/v0.16.0/trl/trainer/ppo_trainer.py#L513
+
+ Args:
+ token_level_rewards: `(torch.Tensor)`
+ shape: (bs, response_length)
+ values: `(torch.Tensor)`
+ shape: (bs, response_length)
+ response_mask: `(torch.Tensor)`
+ shape: (bs, response_length). The token after eos tokens have mask zero.
+ gamma: `(float)`
+ discounted factor used in RL
+ lam: `(float)`
+ lambda value when computing Generalized Advantage Estimation (https://arxiv.org/abs/1506.02438)
+
+ Returns:
+ advantages: `(torch.Tensor)`
+ shape: (bs, response_length)
+ returns: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ """
+ lastgaelam = 0
+ advantages_reversed = []
+ gen_len = token_level_rewards.shape[-1]
+ for t in reversed(range(gen_len)):
+ nextvalues = values[:, t + 1] if t < gen_len - 1 else 0.0
+ delta = token_level_rewards[:, t] + gamma * nextvalues - values[:, t]
+ lastgaelam = delta + gamma * lam * lastgaelam
+ advantages_reversed.append(lastgaelam)
+
+ advantages = torch.stack(advantages_reversed[::-1], dim=1)
+ returns = advantages + values
+ advantages = VF.masked_whiten(advantages, response_mask)
+ return advantages, returns
+
+
+# NOTE(sgm): this implementation only consider outcome supervision, where the reward is a scalar.
+@torch.no_grad()
+def compute_grpo_outcome_advantage(
+ token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor, eps: float = 1e-6
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Compute advantage for GRPO, operating only on Outcome reward
+ (with only one scalar reward for each response).
+
+ Args:
+ token_level_rewards: `(torch.Tensor)`
+ shape: (bs, response_length)
+ response_mask: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ Returns:
+ advantages: `(torch.Tensor)`
+ shape: (bs, response_length)
+ returns: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ """
+ scores = token_level_rewards.sum(dim=-1)
+ id2score = defaultdict(list)
+ id2mean, id2std = {}, {}
+
+ bsz = scores.shape[0]
+ for i in range(bsz):
+ id2score[index[i]].append(scores[i])
+
+ for idx in id2score:
+ assert len(id2score[idx]) > 1, "GRPO needs rollout.n > 1."
+ id2mean[idx] = torch.mean(torch.tensor(id2score[idx]))
+ id2std[idx] = torch.std(torch.tensor(id2score[idx]))
+
+ for i in range(bsz):
+ scores[i] = (scores[i] - id2mean[index[i]]) / (id2std[index[i]] + eps)
+
+ returns = scores.unsqueeze(-1) * response_mask
+ return returns, returns
+
+
+@torch.no_grad()
+def compute_rloo_outcome_advantage(
+ token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Compute advantage for RLOO based on https://arxiv.org/abs/2402.14740
+
+ Args:
+ token_level_rewards: `(torch.Tensor)`
+ shape: (bs, response_length)
+ response_mask: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ Returns:
+ advantages: `(torch.Tensor)`
+ shape: (bs, response_length)
+ returns: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ """
+ scores = token_level_rewards.sum(dim=-1)
+
+ id2score = defaultdict(list)
+ id2sum = {}
+ bsz = scores.shape[0]
+ for i in range(bsz):
+ id2score[index[i]].append(scores[i])
+
+ for idx in id2score:
+ id2sum[idx] = torch.sum(torch.tensor(id2score[idx]))
+
+ for i in range(bsz):
+ sample_num = len(id2score[index[i]])
+ assert sample_num > 1, "RLOO needs rollout.n > 1."
+ baseline = (id2sum[index[i]] - scores[i]) / (sample_num - 1)
+ scores[i] = scores[i] - baseline
+
+ returns = scores.unsqueeze(-1) * response_mask
+ return returns, returns
+
+
+@torch.no_grad()
+def compute_reinforce_plus_plus_outcome_advantage(
+ token_level_rewards: torch.Tensor, response_mask: torch.Tensor, gamma: torch.Tensor
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Compute advantage for REINFORCE++.
+ This implementation is based on the paper: https://arxiv.org/abs/2501.03262
+
+ Args:
+ token_level_rewards: `(torch.Tensor)`
+ shape: (bs, response_length)
+ response_mask: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ Returns:
+ advantages: `(torch.Tensor)`
+ shape: (bs, response_length)
+ returns: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ """
+ returns = torch.zeros_like(token_level_rewards)
+ running_return = 0
+ for t in reversed(range(token_level_rewards.shape[1])):
+ running_return = token_level_rewards[:, t] + gamma * running_return
+ returns[:, t] = running_return
+ # Reset after EOS
+ running_return = running_return * response_mask[:, t]
+
+ advantages = VF.masked_whiten(returns, response_mask)
+ return advantages, returns
+
+
+@torch.no_grad()
+def compute_remax_outcome_advantage(
+ token_level_rewards: torch.Tensor, reward_baselines: torch.Tensor, response_mask: torch.Tensor
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Compute advantage for ReMax, operating only on Outcome reward
+ This implementation is based on the paper: https://arxiv.org/abs/2310.10505
+
+ (with only one scalar reward for each response).
+ Args:
+ token_level_rewards: `(torch.Tensor)`
+ shape: (bs, response_length)
+ reward_baselines: `(torch.Tensor)`
+ shape: (bs,)
+ response_mask: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ Returns:
+ advantages: `(torch.Tensor)`
+ shape: (bs, response_length)
+ returns: `(torch.Tensor)`
+ shape: (bs, response_length)
+
+ """
+ scores = token_level_rewards.sum(dim=-1) - reward_baselines
+ returns = scores.unsqueeze(-1) * response_mask
+ return returns, returns
+
+
+def compute_rewards(
+ token_level_scores: torch.Tensor,
+ log_probs: torch.Tensor,
+ ref_log_probs: torch.Tensor,
+ kl_ratio: float,
+) -> torch.Tensor:
+ kl = log_probs - ref_log_probs
+ return token_level_scores - kl * kl_ratio
+
+
+def compute_policy_loss(
+ old_log_probs: torch.Tensor,
+ log_probs: torch.Tensor,
+ advantages: torch.Tensor,
+ response_mask: torch.Tensor,
+ clip_ratio_low: float,
+ clip_ratio_high: float,
+ clip_ratio_dual: float,
+) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
+ """Compute the policy loss.
+
+ Adapted from https://github.com/huggingface/trl/blob/v0.15.0/trl/trainer/ppo_trainer.py#L568
+
+ Args:
+ old_log_prob: `(torch.Tensor)`
+ shape: (bs, response_length)
+ log_prob: `(torch.Tensor)`
+ shape: (bs, response_length)
+ advantages: `(torch.Tensor)`
+ shape: (bs, response_length)
+ response_mask: `(torch.Tensor)`
+ shape: (bs, response_length)
+ clip_ratio_low: (float)
+ The lower clip range used in PPO. See https://arxiv.org/abs/1707.06347
+ clip_ratio_high: (float)
+ The higher clip range used in DAPO. See https://arxiv.org/pdf/2503.14476
+ clip_ratio_dual: (float)
+ The dual clip range used in Dual-clip PPO. See https://arxiv.org/pdf/1912.09729
+
+ Returns:
+ pg_loss: `a scalar torch.Tensor`
+ policy gradient loss computed via PPO
+ pg_clipfrac_higher: (float)
+ a float number indicating the fraction of policy gradient loss being clipped to a higher value
+ pg_clipfrac_lower: (float)
+ a float number indicating the fraction of policy gradient loss being clipped to a lower value
+ ppo_kl: (float)
+ a float number indicating the mean KL divergence between the old policy and the new policy
+
+ """
+ negative_approx_kl = log_probs - old_log_probs
+ # clamp the ratio before exp to avoid nan
+ # see: https://github.com/pytorch/pytorch/issues/10729
+ ratio = torch.exp(negative_approx_kl)
+ clipped_ratio = torch.exp(
+ torch.clamp(negative_approx_kl, np.log(1.0 - clip_ratio_low), np.log(1.0 + clip_ratio_high))
+ )
+
+ pg_loss = -advantages * ratio
+ pg_loss2 = -advantages * clipped_ratio
+ pg_loss3 = -advantages * clip_ratio_dual
+
+ clipped_pg_loss_higher = torch.max(pg_loss, pg_loss2) # clip if pg_loss < pg_loss2
+ pg_clipfrac_higher = (pg_loss < pg_loss2).float()
+ clipped_pg_loss_lower = torch.min(clipped_pg_loss_higher, pg_loss3) # clip if pg_loss > pg_loss3 and adv < 0
+ final_pg_loss = torch.where(advantages < 0, clipped_pg_loss_lower, clipped_pg_loss_higher)
+ pg_clipfrac_lower = (clipped_pg_loss_higher > pg_loss3).float() * (advantages < 0).float()
+
+ final_pg_loss = VF.masked_mean(final_pg_loss, response_mask)
+ pg_clipfrac_higher = VF.masked_mean(pg_clipfrac_higher, response_mask)
+ pg_clipfrac_lower = VF.masked_mean(pg_clipfrac_lower, response_mask)
+ ppo_kl = VF.masked_mean(-negative_approx_kl, response_mask)
+ return final_pg_loss, pg_clipfrac_higher, pg_clipfrac_lower, ppo_kl
+
+
+def compute_value_loss(
+ vpreds: torch.Tensor,
+ returns: torch.Tensor,
+ values: torch.Tensor,
+ action_mask: torch.Tensor,
+ cliprange_value: float,
+) -> Tuple[torch.Tensor, float]:
+ """Compute the value loss.
+
+ Adapted from https://github.com/huggingface/trl/blob/v0.15.0/trl/trainer/ppo_trainer.py#L556
+
+ Args:
+ vpreds (`torch.FloatTensor`):
+ Predicted values of the value head, shape (`batch_size`, `response_length`)
+ returns: (`torch.FloatTensor`):
+ Ground truth returns, shape (`batch_size`, `response_length`)
+ values (`torch.FloatTensor`):
+ Old values of value head, shape (`batch_size`, `response_length`)
+ action_mask: `(torch.Tensor)`
+ shape: (bs, response_length)
+ cliprange_value: (float)
+ The clip range for value net used in PPO. See https://arxiv.org/abs/1707.06347
+
+ Returns:
+ vf_loss: a scalar (`torch.FloatTensor`):
+ value function loss
+ vf_clipfrac: a float
+ The ratio of vf being clipped
+
+ """
+ vpredclipped = torch.clamp(vpreds, values - cliprange_value, values + cliprange_value)
+ vf_loss1 = torch.square(vpreds - returns)
+ vf_loss2 = torch.square(vpredclipped - returns)
+ vf_loss = 0.5 * VF.masked_mean(torch.max(vf_loss1, vf_loss2), action_mask) # clip if vf_loss1 < vf_loss2
+ vf_clipfrac = VF.masked_mean((vf_loss1 < vf_loss2).float(), action_mask)
+ return vf_loss, vf_clipfrac
+
+
+def compute_kl(log_probs: torch.FloatTensor, ref_log_probs: torch.FloatTensor, kl_penalty: str) -> torch.Tensor:
+ """Compute KL divergence given log_probs and ref_log_probs.
+
+ Adapted from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L1150
+
+ Args:
+ log_probs: torch.Tensor
+ ref_log_probs: torch.Tensor
+ kl_penalty: str
+
+ Returns:
+ kl_div: torch.Tensor
+
+ """
+ log_probs, ref_log_probs = log_probs.float(), ref_log_probs.float()
+ if kl_penalty == "kl":
+ return log_probs - ref_log_probs
+
+ if kl_penalty == "abs":
+ return (log_probs - ref_log_probs).abs()
+
+ if kl_penalty == "mse":
+ return 0.5 * (log_probs - ref_log_probs).square()
+
+ # J. Schulman. Approximating kl divergence, 2020.
+ # URL http://joschu.net/blog/kl-approx.html
+ if kl_penalty == "low_var_kl":
+ kl = ref_log_probs - log_probs
+ kld = (kl.exp() - kl - 1).contiguous()
+ return torch.clamp(kld, min=-10, max=10)
+
+ if kl_penalty == "full":
+ return F.kl_div(ref_log_probs, log_probs, log_target=True, reduction="none").sum(-1)
+
+ raise NotImplementedError(f"Unknown KL penalty: {kl_penalty}.")
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/data_loader.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/data_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..14390310395dfcb22081937c2111400cda1f9f43
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/data_loader.py
@@ -0,0 +1,88 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional
+
+import torch
+from torch.utils.data import RandomSampler, SequentialSampler
+from torchdata.stateful_dataloader import StatefulDataLoader
+from transformers import PreTrainedTokenizer, ProcessorMixin
+
+from ..utils.dataset import RLHFDataset, collate_fn
+from .config import DataConfig
+
+
+def create_dataloader(config: DataConfig, tokenizer: PreTrainedTokenizer, processor: Optional[ProcessorMixin]) -> None:
+ train_dataset = RLHFDataset(
+ data_path=config.train_files,
+ tokenizer=tokenizer,
+ processor=processor,
+ prompt_key=config.prompt_key,
+ answer_key=config.answer_key,
+ image_key=config.image_key,
+ max_prompt_length=config.max_prompt_length,
+ truncation="right",
+ format_prompt=config.format_prompt,
+ min_pixels=config.min_pixels,
+ max_pixels=config.max_pixels,
+ filter_overlong_prompts=config.filter_overlong_prompts,
+ )
+ # use sampler for better ckpt resume
+ if config.shuffle:
+ train_dataloader_generator = torch.Generator()
+ train_dataloader_generator.manual_seed(config.seed)
+ print("train_dataset size:", len(train_dataset))
+ sampler = RandomSampler(data_source=train_dataset, generator=train_dataloader_generator)
+ else:
+ sampler = SequentialSampler(data_source=train_dataset)
+
+ train_dataloader = StatefulDataLoader(
+ dataset=train_dataset,
+ batch_size=config.rollout_batch_size,
+ sampler=sampler,
+ num_workers=8,
+ collate_fn=collate_fn,
+ pin_memory=False,
+ drop_last=True,
+ )
+
+ val_dataset = RLHFDataset(
+ data_path=config.val_files,
+ tokenizer=tokenizer,
+ processor=processor,
+ prompt_key=config.prompt_key,
+ answer_key=config.answer_key,
+ image_key=config.image_key,
+ max_prompt_length=config.max_prompt_length,
+ truncation="right",
+ format_prompt=config.format_prompt,
+ min_pixels=config.min_pixels,
+ max_pixels=config.max_pixels,
+ filter_overlong_prompts=config.filter_overlong_prompts,
+ )
+ val_dataloader = StatefulDataLoader(
+ dataset=val_dataset,
+ batch_size=len(val_dataset) if config.val_batch_size == -1 else config.val_batch_size,
+ shuffle=False,
+ num_workers=8,
+ collate_fn=collate_fn,
+ pin_memory=False,
+ drop_last=False,
+ )
+
+ assert len(train_dataloader) >= 1
+ assert len(val_dataloader) >= 1
+ print(f"Size of train dataloader: {len(train_dataloader)}")
+ print(f"Size of val dataloader: {len(val_dataloader)}")
+ return train_dataloader, val_dataloader
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/main.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..cdba33cfb6b89f17d65cb824c477aed770a93bfb
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/main.py
@@ -0,0 +1,130 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+
+import ray
+from omegaconf import OmegaConf
+
+from ..single_controller.ray import RayWorkerGroup
+from ..utils.tokenizer import get_processor, get_tokenizer
+from ..workers.fsdp_workers import FSDPWorker
+from ..workers.reward import BatchFunctionRewardManager, SequentialFunctionRewardManager
+from .config import PPOConfig
+from .data_loader import create_dataloader
+from .ray_trainer import RayPPOTrainer, ResourcePoolManager, Role
+
+
+# please make sure main_task is not scheduled on head
+@ray.remote(num_cpus=1)
+class Runner:
+ """A runner for RL training."""
+
+ def run(self, config: PPOConfig):
+ # print config
+ print(json.dumps(config.to_dict(), indent=2))
+
+ # instantiate tokenizer
+ tokenizer = get_tokenizer(
+ config.worker.actor.model.model_path,
+ override_chat_template=config.data.override_chat_template,
+ trust_remote_code=config.worker.actor.model.trust_remote_code,
+ use_fast=True,
+ )
+ processor = get_processor(
+ config.worker.actor.model.model_path,
+ override_chat_template=config.data.override_chat_template,
+ trust_remote_code=config.worker.actor.model.trust_remote_code,
+ use_fast=True,
+ )
+
+ # define worker classes
+ ray_worker_group_cls = RayWorkerGroup
+ role_worker_mapping = {
+ Role.ActorRollout: ray.remote(FSDPWorker),
+ Role.Critic: ray.remote(FSDPWorker),
+ Role.RefPolicy: ray.remote(FSDPWorker),
+ }
+ global_pool_id = "global_pool"
+ resource_pool_spec = {
+ global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes,
+ }
+ mapping = {
+ Role.ActorRollout: global_pool_id,
+ Role.Critic: global_pool_id,
+ Role.RefPolicy: global_pool_id,
+ }
+ resource_pool_manager = ResourcePoolManager(resource_pool_spec=resource_pool_spec, mapping=mapping)
+
+ if config.worker.reward.reward_type == "sequential":
+ RewardManager = SequentialFunctionRewardManager
+ elif config.worker.reward.reward_type == "batch":
+ RewardManager = BatchFunctionRewardManager
+ else:
+ raise NotImplementedError(f"Unknown reward type {config.worker.reward.reward_type}.")
+
+ RemoteRewardManager = ray.remote(RewardManager).options(num_cpus=config.worker.reward.num_cpus)
+ reward_fn = RemoteRewardManager.remote(config.worker.reward, tokenizer)
+ val_reward_fn = RemoteRewardManager.remote(config.worker.reward, tokenizer)
+
+ train_dataloader, val_dataloader = create_dataloader(config.data, tokenizer, processor)
+
+ trainer = RayPPOTrainer(
+ config=config,
+ tokenizer=tokenizer,
+ processor=processor,
+ train_dataloader=train_dataloader,
+ val_dataloader=val_dataloader,
+ role_worker_mapping=role_worker_mapping,
+ resource_pool_manager=resource_pool_manager,
+ ray_worker_group_cls=ray_worker_group_cls,
+ reward_fn=reward_fn,
+ val_reward_fn=val_reward_fn,
+ )
+ trainer.init_workers()
+ trainer.fit()
+
+
+def main():
+ cli_args = OmegaConf.from_cli()
+ default_config = OmegaConf.structured(PPOConfig())
+
+ if hasattr(cli_args, "config"):
+ config_path = cli_args.pop("config", None)
+ file_config = OmegaConf.load(config_path)
+ default_config = OmegaConf.merge(default_config, file_config)
+
+ ppo_config = OmegaConf.merge(default_config, cli_args)
+ ppo_config: PPOConfig = OmegaConf.to_object(ppo_config)
+ ppo_config.deep_post_init()
+
+ if not ray.is_initialized():
+ runtime_env = {
+ "env_vars": {
+ "TOKENIZERS_PARALLELISM": "true",
+ "NCCL_DEBUG": "WARN",
+ "VLLM_LOGGING_LEVEL": "WARN",
+ "TORCH_NCCL_AVOID_RECORD_STREAMS": "1",
+ "PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:False",
+ "PYTHONUNBUFFERED": "1",
+ }
+ }
+ ray.init(runtime_env=runtime_env)
+
+ runner = Runner.remote()
+ ray.get(runner.run.remote(ppo_config))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/metrics.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/metrics.py
new file mode 100644
index 0000000000000000000000000000000000000000..02cd2335f9ac1aada4fcbe795940d94d5b99281b
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/metrics.py
@@ -0,0 +1,120 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Any, Dict, List
+
+import numpy as np
+import torch
+
+from ..protocol import DataProto
+
+
+def reduce_metrics(metrics: Dict[str, List[Any]]) -> Dict[str, Any]:
+ return {key: np.mean(value) for key, value in metrics.items()}
+
+
+def compute_data_metrics(batch: DataProto, use_critic: bool = False) -> Dict[str, Any]:
+ sequence_score = batch.batch["token_level_scores"].sum(-1)
+ sequence_reward = batch.batch["token_level_rewards"].sum(-1)
+
+ advantages = batch.batch["advantages"]
+ returns = batch.batch["returns"]
+
+ max_response_length = batch.batch["responses"].size(-1)
+
+ prompt_mask = batch.batch["attention_mask"][:, :-max_response_length].bool()
+ response_mask = batch.batch["attention_mask"][:, -max_response_length:].bool()
+
+ max_prompt_length = prompt_mask.size(-1)
+ prompt_length = prompt_mask.sum(-1).float()
+ response_length = response_mask.sum(-1).float()
+
+ valid_adv = torch.masked_select(advantages, response_mask)
+ valid_returns = torch.masked_select(returns, response_mask)
+
+ if use_critic:
+ values = batch.batch["values"]
+ valid_values = torch.masked_select(values, response_mask)
+ return_diff_var = torch.var(valid_returns - valid_values)
+ return_var = torch.var(valid_returns)
+
+ metrics = {
+ # score
+ "critic/score/mean": torch.mean(sequence_score).detach().item(),
+ "critic/score/max": torch.max(sequence_score).detach().item(),
+ "critic/score/min": torch.min(sequence_score).detach().item(),
+ # reward
+ "critic/rewards/mean": torch.mean(sequence_reward).detach().item(),
+ "critic/rewards/max": torch.max(sequence_reward).detach().item(),
+ "critic/rewards/min": torch.min(sequence_reward).detach().item(),
+ # adv
+ "critic/advantages/mean": torch.mean(valid_adv).detach().item(),
+ "critic/advantages/max": torch.max(valid_adv).detach().item(),
+ "critic/advantages/min": torch.min(valid_adv).detach().item(),
+ # returns
+ "critic/returns/mean": torch.mean(valid_returns).detach().item(),
+ "critic/returns/max": torch.max(valid_returns).detach().item(),
+ "critic/returns/min": torch.min(valid_returns).detach().item(),
+ **(
+ {
+ # values
+ "critic/values/mean": torch.mean(valid_values).detach().item(),
+ "critic/values/max": torch.max(valid_values).detach().item(),
+ "critic/values/min": torch.min(valid_values).detach().item(),
+ # vf explained var
+ "critic/vf_explained_var": (1.0 - return_diff_var / (return_var + 1e-5)).detach().item(),
+ }
+ if use_critic
+ else {}
+ ),
+ # response length
+ "response_length/mean": torch.mean(response_length).detach().item(),
+ "response_length/max": torch.max(response_length).detach().item(),
+ "response_length/min": torch.min(response_length).detach().item(),
+ "response_length/clip_ratio": torch.mean(torch.eq(response_length, max_response_length).float())
+ .detach()
+ .item(),
+ # prompt length
+ "prompt_length/mean": torch.mean(prompt_length).detach().item(),
+ "prompt_length/max": torch.max(prompt_length).detach().item(),
+ "prompt_length/min": torch.min(prompt_length).detach().item(),
+ "prompt_length/clip_ratio": torch.mean(torch.eq(prompt_length, max_prompt_length).float()).detach().item(),
+ }
+ return metrics
+
+
+def compute_timing_metrics(batch: DataProto, timing_raw: Dict[str, float]) -> Dict[str, Any]:
+ num_response_tokens = torch.sum(batch.batch["response_mask"]).item()
+ num_overall_tokens = sum(batch.meta_info["global_token_num"])
+ num_tokens_of_section = {
+ **dict.fromkeys(["gen", "reward"], num_response_tokens),
+ **dict.fromkeys(["ref", "old", "values", "adv", "update_critic", "update_actor"], num_overall_tokens),
+ }
+ return {
+ **{f"timing_s/{name}": value for name, value in timing_raw.items()},
+ **{
+ f"timing_per_token_ms/{name}": timing_raw[name] * 1000 / num_tokens_of_section[name]
+ for name in set(num_tokens_of_section.keys()) & set(timing_raw.keys())
+ },
+ }
+
+
+def compute_throughout_metrics(batch: DataProto, timing_raw: Dict[str, float], num_gpus: int) -> Dict[str, Any]:
+ total_num_tokens = sum(batch.meta_info["global_token_num"])
+ time = timing_raw["step"]
+ return {
+ "perf/total_num_tokens": total_num_tokens,
+ "perf/time_per_step": time,
+ "perf/throughput": total_num_tokens / (time * num_gpus),
+ }
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/ray_trainer.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/ray_trainer.py
new file mode 100644
index 0000000000000000000000000000000000000000..784ff9a782b9421714be9529dc0325fa1f0c86d2
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/ray_trainer.py
@@ -0,0 +1,637 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+FSDP PPO Trainer with Ray-based single controller.
+This trainer supports model-agonistic model initialization with huggingface
+"""
+
+import os
+import uuid
+from collections import defaultdict
+from copy import deepcopy
+from dataclasses import dataclass, field
+from enum import Enum, IntEnum, auto
+from typing import Any, Dict, List, Optional, Type
+
+import numpy as np
+import ray
+import torch
+from ray.experimental.tqdm_ray import tqdm
+from torchdata.stateful_dataloader import StatefulDataLoader
+from transformers import PreTrainedTokenizer, ProcessorMixin
+
+from ..protocol import DataProto, pad_dataproto_to_divisor, unpad_dataproto
+from ..single_controller.base import Worker
+from ..single_controller.ray import RayClassWithInitArgs, RayResourcePool, RayWorkerGroup
+from ..single_controller.ray.base import create_colocated_worker_cls
+from ..utils import torch_functional as VF
+from ..utils.checkpoint import CHECKPOINT_TRACKER, remove_obsolete_ckpt
+from ..utils.logger import Tracker
+from ..utils.py_functional import convert_dict_to_str, timer
+from ..utils.seqlen_balancing import get_seqlen_balanced_partitions, log_seqlen_unbalance
+from ..workers.fsdp_workers import FSDPWorker
+from ..workers.reward import FunctionRewardManager
+from . import core_algos
+from .config import PPOConfig
+from .metrics import compute_data_metrics, compute_throughout_metrics, compute_timing_metrics, reduce_metrics
+
+
+class Role(IntEnum):
+ """
+ To create more roles dynamically, you can subclass Role and add new members
+ """
+
+ Actor = auto()
+ Rollout = auto()
+ ActorRollout = auto()
+ Critic = auto()
+ RefPolicy = auto()
+ RewardModel = auto()
+ ActorRolloutRef = auto()
+
+
+class AdvantageEstimator(str, Enum):
+ """
+ Using an enumeration class to avoid spelling errors in adv_estimator
+ """
+
+ GAE = "gae"
+ GRPO = "grpo"
+ REINFORCE_PLUS_PLUS = "reinforce_plus_plus"
+ REMAX = "remax"
+ RLOO = "rloo"
+
+
+@dataclass
+class ResourcePoolManager:
+ """
+ Define a resource pool specification. Resource pool will be initialized first.
+ """
+
+ resource_pool_spec: dict[str, list[int]]
+ mapping: dict[Role, str]
+ resource_pool_dict: dict[str, RayResourcePool] = field(default_factory=dict)
+
+ def create_resource_pool(self):
+ for resource_pool_name, process_on_nodes in self.resource_pool_spec.items():
+ # max_colocate_count means the number of WorkerGroups (i.e. processes) in each RayResourcePool
+ # For FSDP backend, we recommend using max_colocate_count=1 that merge all WorkerGroups into one.
+ # For Megatron backend, we recommend using max_colocate_count>1 that can utilize different WorkerGroup for differnt models
+ resource_pool = RayResourcePool(
+ process_on_nodes=process_on_nodes, use_gpu=True, max_colocate_count=1, name_prefix=resource_pool_name
+ )
+ self.resource_pool_dict[resource_pool_name] = resource_pool
+
+ self._check_resource_available()
+
+ def get_resource_pool(self, role: Role) -> RayResourcePool:
+ """Get the resource pool of the worker."""
+ return self.resource_pool_dict[self.mapping[role]]
+
+ def get_num_gpus(self) -> int:
+ """Get the number of gpus in this cluster."""
+ return sum([n_gpus for process_on_nodes in self.resource_pool_spec.values() for n_gpus in process_on_nodes])
+
+ def _check_resource_available(self):
+ """Check if the resource pool can be satisfied in this ray cluster."""
+ gpus_available = ray.available_resources().get("GPU", 0)
+ gpus_required = self.get_num_gpus()
+ if gpus_available < gpus_required:
+ raise ValueError(f"Total available GPUs {gpus_available} is less than total desired GPUs {gpus_required}.")
+
+
+def apply_kl_penalty(data: DataProto, kl_ctrl: core_algos.KLController, kl_penalty="kl"):
+ token_level_scores = data.batch["token_level_scores"]
+ batch_size = data.batch.batch_size[0]
+ response_mask = data.batch["response_mask"]
+
+ # compute kl between ref_policy and current policy
+ kld = core_algos.compute_kl(data.batch["old_log_probs"], data.batch["ref_log_probs"], kl_penalty=kl_penalty)
+ kld = kld * response_mask # (batch_size, response_length)
+
+ data.batch["token_level_rewards"] = token_level_scores - kl_ctrl.kl_coef * kld
+
+ current_kl = VF.masked_mean(kld, mask=response_mask, dim=-1) # average over sequence
+ current_kl = torch.mean(current_kl, dim=0).item()
+ metrics = {"critic/kl": current_kl, "critic/kl_coef": kl_ctrl.kl_coef}
+
+ # According to https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L880
+ kl_ctrl.update(current_kl=current_kl, n_steps=batch_size)
+ return data, metrics
+
+
+def compute_advantage(data: DataProto, adv_estimator: AdvantageEstimator, gamma: float = 1.0, lam: float = 1.0):
+ token_level_rewards = data.batch["token_level_rewards"]
+ response_mask = data.batch["response_mask"]
+ index = data.non_tensor_batch["uid"]
+ if adv_estimator == AdvantageEstimator.GAE:
+ values = data.batch["values"]
+ advantages, returns = core_algos.compute_gae_advantage_return(
+ token_level_rewards, values, response_mask, gamma, lam
+ )
+ elif adv_estimator == AdvantageEstimator.GRPO:
+ advantages, returns = core_algos.compute_grpo_outcome_advantage(token_level_rewards, response_mask, index)
+ elif adv_estimator == AdvantageEstimator.REINFORCE_PLUS_PLUS:
+ advantages, returns = core_algos.compute_reinforce_plus_plus_outcome_advantage(
+ token_level_rewards, response_mask, gamma
+ )
+ elif adv_estimator == AdvantageEstimator.REMAX:
+ reward_baselines = data.batch["reward_baselines"]
+ advantages, returns = core_algos.compute_remax_outcome_advantage(
+ token_level_rewards, reward_baselines, response_mask
+ )
+ elif adv_estimator == AdvantageEstimator.RLOO:
+ advantages, returns = core_algos.compute_rloo_outcome_advantage(token_level_rewards, response_mask, index)
+ else:
+ raise NotImplementedError
+
+ data.batch["advantages"] = advantages
+ data.batch["returns"] = returns
+ return data
+
+
+class RayPPOTrainer:
+ """
+ Note that this trainer runs on the driver process on a single CPU/GPU node.
+ """
+
+ def __init__(
+ self,
+ config: PPOConfig,
+ tokenizer: PreTrainedTokenizer,
+ processor: Optional[ProcessorMixin],
+ train_dataloader: StatefulDataLoader,
+ val_dataloader: StatefulDataLoader,
+ role_worker_mapping: dict[Role, Type[Worker]],
+ resource_pool_manager: ResourcePoolManager,
+ ray_worker_group_cls: Type[RayWorkerGroup] = RayWorkerGroup,
+ reward_fn: Optional[FunctionRewardManager] = None,
+ val_reward_fn: Optional[FunctionRewardManager] = None,
+ ):
+ self.tokenizer = tokenizer
+ self.processor = processor
+ self.train_dataloader = train_dataloader
+ self.val_dataloader = val_dataloader
+ self.config = config
+ self.reward_fn = reward_fn
+ self.val_reward_fn = val_reward_fn
+
+ self.hybrid_engine = config.worker.hybrid_engine
+ if self.hybrid_engine:
+ assert Role.ActorRollout in role_worker_mapping, (
+ f"ActorRollout should be included in {role_worker_mapping.keys()}."
+ )
+ else:
+ raise NotImplementedError
+
+ self.role_worker_mapping = role_worker_mapping
+ self.resource_pool_manager = resource_pool_manager
+ self.use_reward_model = Role.RewardModel in role_worker_mapping
+ self.ray_worker_group_cls = ray_worker_group_cls
+
+ # define KL control
+ if Role.RefPolicy in role_worker_mapping and not config.algorithm.disable_kl:
+ self.use_reference_policy = True
+ self.kl_ctrl = core_algos.get_kl_controller(config.algorithm)
+ else:
+ self.use_reference_policy = False
+ self.kl_ctrl = core_algos.FixedKLController(init_kl_coef=0.0)
+ print("KL is disabled, no KL metrics will be logged. Please set `kl_coef=0` to log KL metrics.")
+
+ if config.algorithm.adv_estimator == AdvantageEstimator.GAE:
+ self.use_critic = True
+ else:
+ self.use_critic = False
+
+ if config.algorithm.adv_estimator not in list(AdvantageEstimator):
+ raise NotImplementedError(f"Unknown advantage estimator: {config.algorithm.adv_estimator}.")
+
+ if config.data.rollout_batch_size % config.worker.actor.global_batch_size != 0:
+ raise ValueError("Rollout batch size must be divisible by actor global batch size.")
+
+ if (
+ config.data.rollout_batch_size * config.worker.rollout.n
+ ) % config.worker.actor.micro_batch_size_per_device_for_experience != 0:
+ raise ValueError(
+ "Rollout batch size * rollout.n must be divisible by actor micro batch size for experience."
+ )
+
+ if self.use_critic:
+ if config.data.rollout_batch_size % config.worker.critic.global_batch_size != 0:
+ raise ValueError("Rollout batch size must be divisible by critic global batch size.")
+
+ if (
+ config.data.rollout_batch_size * config.worker.rollout.n
+ ) % config.worker.critic.micro_batch_size_per_device_for_experience != 0:
+ raise ValueError(
+ "Rollout batch size * rollout.n must be divisible by critic micro batch size for experience."
+ )
+
+ if (
+ config.algorithm.adv_estimator in (AdvantageEstimator.GRPO, AdvantageEstimator.RLOO)
+ and config.worker.rollout.n == 1
+ ):
+ raise ValueError("GRPO and RLOO algorithm need `config.worker.rollout.n > 1`.")
+
+ if config.trainer.max_steps is not None:
+ self.training_steps = config.trainer.max_steps
+ else:
+ self.training_steps = len(train_dataloader) * config.trainer.total_epochs
+
+ config.worker.actor.optim.training_steps = self.training_steps
+ config.worker.critic.optim.training_steps = self.training_steps
+ print(f"Total training steps: {self.training_steps}")
+
+ def _maybe_log_val_generations(
+ self, inputs: List[str], outputs: List[str], labels: List[str], scores: List[float]
+ ) -> None:
+ """Log a table of validation samples"""
+ if self.config.trainer.val_generations_to_log <= 0:
+ return
+
+ # Create tuples of (input, output, score) and sort by input text
+ samples = list(zip(inputs, outputs, labels, scores))
+ samples.sort(key=lambda x: x[0]) # Sort by input text
+
+ # Use fixed random seed for deterministic shuffling
+ rng = np.random.RandomState(42)
+ rng.shuffle(samples)
+
+ samples = samples[: self.config.trainer.val_generations_to_log]
+ self.logger.log_generation(samples, self.global_step)
+
+ def _validate(self) -> Dict[str, Any]:
+ reward_tensor_lst = []
+ # Lists to collect samples for the table
+ sample_inputs, sample_outputs, sample_labels, sample_scores = [], [], [], []
+ reward_metrics_lst = defaultdict(list)
+ for batch_dict in self.val_dataloader:
+ test_batch = DataProto.from_single_dict(batch_dict)
+ # Store original inputs
+ input_ids = test_batch.batch["input_ids"]
+ input_texts = [self.tokenizer.decode(ids, skip_special_tokens=True) for ids in input_ids]
+ sample_inputs.extend(input_texts)
+
+ if "multi_modal_data" in test_batch.non_tensor_batch.keys():
+ test_gen_batch = test_batch.pop(
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
+ non_tensor_batch_keys=["raw_prompt_ids", "multi_modal_data"],
+ )
+ else:
+ test_gen_batch = test_batch.pop(
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
+ non_tensor_batch_keys=["raw_prompt_ids"],
+ )
+
+ test_gen_batch.meta_info = self.config.worker.rollout.val_override_config
+ test_gen_batch.meta_info.update({
+ "min_pixels": self.config.data.min_pixels,
+ "max_pixels": self.config.data.max_pixels,
+ })
+ test_gen_batch, pad_size = pad_dataproto_to_divisor(test_gen_batch, self.actor_rollout_wg.world_size)
+ test_output_gen_batch = self.actor_rollout_wg.generate_sequences(test_gen_batch)
+ test_output_gen_batch = unpad_dataproto(test_output_gen_batch, pad_size=pad_size)
+
+ # Store generated outputs
+ output_ids = test_output_gen_batch.batch["responses"]
+ output_texts = [self.tokenizer.decode(ids, skip_special_tokens=True) for ids in output_ids]
+ sample_outputs.extend(output_texts)
+ sample_labels.extend(test_batch.non_tensor_batch["ground_truth"].tolist())
+ test_batch = test_batch.union(test_output_gen_batch)
+
+ # evaluate using reward_function
+ reward_tensor, reward_metrics = ray.get(self.val_reward_fn.compute_reward.remote(test_batch))
+
+ # Store scores
+ scores = reward_tensor.sum(-1).cpu().tolist()
+ sample_scores.extend(scores)
+
+ reward_tensor_lst.append(reward_tensor)
+ for key, value in reward_metrics.items():
+ reward_metrics_lst[key].extend(value)
+
+ self._maybe_log_val_generations(sample_inputs, sample_outputs, sample_labels, sample_scores)
+ reward_score = torch.cat(reward_tensor_lst, dim=0).sum(-1).mean().item()
+ val_reward_metrics = {f"val/{key}_reward": value for key, value in reduce_metrics(reward_metrics_lst).items()}
+ return {"val/reward_score": reward_score, **val_reward_metrics}
+
+ def init_workers(self) -> None:
+ """Init resource pool and worker group"""
+ self.resource_pool_manager.create_resource_pool()
+ self.resource_pool_to_cls = {pool: {} for pool in self.resource_pool_manager.resource_pool_dict.values()}
+
+ # create actor and rollout
+ if self.hybrid_engine:
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.ActorRollout)
+ actor_rollout_cls = RayClassWithInitArgs(
+ cls=self.role_worker_mapping[Role.ActorRollout], config=self.config.worker, role="actor_rollout"
+ )
+ self.resource_pool_to_cls[resource_pool]["actor_rollout"] = actor_rollout_cls
+ else:
+ raise NotImplementedError
+
+ # create critic
+ if self.use_critic:
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.Critic)
+ critic_cls = RayClassWithInitArgs(
+ cls=self.role_worker_mapping[Role.Critic], config=self.config.worker, role="critic"
+ )
+ self.resource_pool_to_cls[resource_pool]["critic"] = critic_cls
+
+ # create reference policy if needed
+ if self.use_reference_policy:
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.RefPolicy)
+ ref_policy_cls = RayClassWithInitArgs(
+ self.role_worker_mapping[Role.RefPolicy], config=self.config.worker, role="ref"
+ )
+ self.resource_pool_to_cls[resource_pool]["ref"] = ref_policy_cls
+
+ # create a reward model if reward_fn is None
+ if self.use_reward_model:
+ # we create a RM here
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.RewardModel)
+ rm_cls = RayClassWithInitArgs(
+ cls=self.role_worker_mapping[Role.RewardModel], config=self.config.worker, role="reward"
+ )
+ self.resource_pool_to_cls[resource_pool]["rm"] = rm_cls
+
+ # initialize WorkerGroup
+ # NOTE: if you want to use a different resource pool for each role, which can support different parallel size,
+ # you should not use `create_colocated_worker_cls`. Instead, directly pass different resource pool to different worker groups.
+ # See https://github.com/volcengine/verl/blob/master/examples/ray/tutorial.ipynb for more information.
+ all_wg: Dict[str, FSDPWorker] = {}
+ self.wg_dicts = []
+ for resource_pool, class_dict in self.resource_pool_to_cls.items():
+ worker_dict_cls = create_colocated_worker_cls(class_dict=class_dict)
+ wg_dict = self.ray_worker_group_cls(resource_pool=resource_pool, ray_cls_with_init=worker_dict_cls)
+ spawn_wg = wg_dict.spawn(prefix_set=class_dict.keys())
+ all_wg.update(spawn_wg)
+ # keep the referece of WorkerDict to support ray >= 2.31. Ref: https://github.com/ray-project/ray/pull/45699
+ self.wg_dicts.append(wg_dict)
+
+ if self.use_critic:
+ self.critic_wg = all_wg["critic"]
+ self.critic_wg.init_model()
+
+ if self.use_reference_policy:
+ self.ref_policy_wg = all_wg["ref"]
+ self.ref_policy_wg.init_model()
+
+ if self.use_reward_model:
+ self.rm_wg = all_wg["rm"]
+ self.rm_wg.init_model()
+
+ # we should create rollout at the end so that vllm can have a better estimation of kv cache memory
+ self.actor_rollout_wg = all_wg["actor_rollout"]
+ self.actor_rollout_wg.init_model()
+
+ def _save_checkpoint(self) -> None:
+ # path: {save_checkpoint_path}/global_step_{global_step}/{actor,critic}
+ remove_obsolete_ckpt(
+ self.config.trainer.save_checkpoint_path, self.global_step, self.config.trainer.save_limit
+ )
+ folder_path = os.path.join(self.config.trainer.save_checkpoint_path, f"global_step_{self.global_step}")
+ actor_path = os.path.join(folder_path, "actor")
+ self.actor_rollout_wg.save_checkpoint(actor_path)
+
+ if self.use_critic:
+ critic_path = os.path.join(folder_path, "critic")
+ self.critic_wg.save_checkpoint(critic_path)
+
+ dataloader_path = os.path.join(folder_path, "dataloader.pt")
+ dataloader_state_dict = self.train_dataloader.state_dict()
+ torch.save(dataloader_state_dict, dataloader_path)
+
+ last_global_step_path = os.path.join(self.config.trainer.save_checkpoint_path, CHECKPOINT_TRACKER)
+ with open(last_global_step_path, "w") as f:
+ f.write(str(self.global_step))
+
+ def _load_checkpoint(self) -> None:
+ if self.config.trainer.load_checkpoint_path is None:
+ return
+
+ if "global_step_" not in self.config.trainer.load_checkpoint_path.strip(os.path.sep).split(os.path.sep)[-1]:
+ raise ValueError("`load_checkpoint_path` should end with `global_step_*`.")
+
+ print(f"Load from checkpoint: {self.config.trainer.load_checkpoint_path}.")
+ self.global_step = int(self.config.trainer.load_checkpoint_path.strip(os.path.sep).split("global_step_")[-1])
+ actor_path = os.path.join(self.config.trainer.load_checkpoint_path, "actor")
+ self.actor_rollout_wg.load_checkpoint(actor_path)
+ if self.use_critic:
+ critic_path = os.path.join(self.config.trainer.load_checkpoint_path, "critic")
+ self.critic_wg.load_checkpoint(critic_path)
+
+ dataloader_path = os.path.join(self.config.trainer.load_checkpoint_path, "dataloader.pt")
+ if os.path.exists(dataloader_path):
+ dataloader_state_dict = torch.load(dataloader_path, weights_only=False)
+ self.train_dataloader.load_state_dict(dataloader_state_dict)
+ else:
+ print(f"No dataloader state found at {dataloader_path}, will start from scratch.")
+
+ def _balance_batch(self, batch: DataProto, metrics: Dict[str, Any], logging_prefix: str = "global_seqlen") -> None:
+ """Reorder the data on single controller such that each dp rank gets similar total tokens"""
+ attention_mask = batch.batch["attention_mask"]
+ batch_size = attention_mask.shape[0]
+ global_seqlen_lst = batch.batch["attention_mask"].view(batch_size, -1).sum(-1).tolist() # (train_batch_size,)
+ world_size = self.actor_rollout_wg.world_size
+ global_partition_lst = get_seqlen_balanced_partitions(
+ global_seqlen_lst, k_partitions=world_size, equal_size=True
+ )
+ # reorder based on index. The data will be automatically equally partitioned by dispatch function
+ global_idx = torch.tensor([j for partition in global_partition_lst for j in partition])
+ batch.reorder(global_idx)
+ global_balance_stats = log_seqlen_unbalance(
+ seqlen_list=global_seqlen_lst, partitions=global_partition_lst, prefix=logging_prefix
+ )
+ metrics.update(global_balance_stats)
+
+ def fit(self):
+ """
+ The training loop of PPO.
+ The driver process only need to call the compute functions of the worker group through RPC to construct the PPO dataflow.
+ The light-weight advantage computation is done on the driver process.
+ """
+ self.logger = Tracker(loggers=self.config.trainer.logger, config=self.config.to_dict())
+ self.global_step = 0
+ val_metrics: Optional[Dict[str, Any]] = None
+
+ # load checkpoint before doing anything
+ self._load_checkpoint()
+
+ # perform validation before training
+ # currently, we only support validation using the reward_function.
+ if self.val_reward_fn is not None and self.config.trainer.val_before_train:
+ val_metrics = self._validate()
+ self.logger.log(data=val_metrics, step=self.global_step)
+ if self.config.trainer.val_only:
+ return
+
+ for _ in tqdm(range(self.config.trainer.total_epochs), desc="Epoch", position=0):
+ for batch_dict in tqdm(self.train_dataloader, desc="Running step", position=1):
+ self.global_step += 1
+ if self.global_step > self.training_steps:
+ break
+
+ metrics, timing_raw = {}, {}
+ batch: DataProto = DataProto.from_single_dict(batch_dict)
+
+ # pop those keys for generation
+ if "multi_modal_data" in batch.non_tensor_batch.keys():
+ print(batch.non_tensor_batch['multi_modal_data'].shape)
+ gen_batch = batch.pop(
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
+ non_tensor_batch_keys=["raw_prompt_ids", "multi_modal_data"],
+ )
+ gen_batch.meta_info.update({
+ "min_pixels": self.config.data.min_pixels,
+ "max_pixels": self.config.data.max_pixels,
+ })
+ else:
+ gen_batch = batch.pop(
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
+ non_tensor_batch_keys=["raw_prompt_ids"],
+ )
+
+ with timer("step", timing_raw):
+ # generate a batch
+ with timer("gen", timing_raw): # wg: worker group
+ gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch)
+
+ if self.config.algorithm.adv_estimator == "remax":
+ with timer("gen_max", timing_raw):
+ gen_baseline_batch = deepcopy(gen_batch)
+ gen_baseline_batch.meta_info["temperature"] = 0
+ gen_baseline_batch.meta_info["n"] = 1
+ gen_baseline_output = self.actor_rollout_wg.generate_sequences(gen_baseline_batch)
+
+ batch = batch.union(gen_baseline_output)
+ reward_baseline_tensor, _ = ray.get(self.reward_fn.compute_reward.remote(batch))
+ reward_baseline_tensor = reward_baseline_tensor.sum(dim=-1)
+
+ batch.pop(batch_keys=list(gen_baseline_output.batch.keys()))
+ batch.batch["reward_baselines"] = reward_baseline_tensor
+ del gen_baseline_batch, gen_baseline_output
+
+ batch.non_tensor_batch["uid"] = np.array(
+ [str(uuid.uuid4()) for _ in range(len(batch.batch))], dtype=object
+ )
+ # repeat to align with repeated responses in rollout
+ batch = batch.repeat(repeat_times=self.config.worker.rollout.n, interleave=True)
+ batch = batch.union(gen_batch_output)
+
+ # balance the number of valid tokens on each dp rank.
+ # Note that this breaks the order of data inside the batch.
+ # Please take care when you implement group based adv computation such as GRPO and rloo
+ self._balance_batch(batch, metrics=metrics)
+
+ # compute global_valid tokens
+ batch.meta_info["global_token_num"] = torch.sum(batch.batch["attention_mask"], dim=-1).tolist()
+
+ # compute reward
+ with timer("reward", timing_raw):
+ reward_ref = self.reward_fn.compute_reward.remote(batch)
+
+ # recompute old_log_probs
+ with timer("old", timing_raw):
+ old_log_probs = self.actor_rollout_wg.compute_log_probs(batch)
+ batch = batch.union(old_log_probs)
+
+ # compute ref_log_probs
+ if self.use_reference_policy:
+ with timer("ref", timing_raw):
+ ref_log_probs = self.ref_policy_wg.compute_ref_log_probs(batch)
+ batch = batch.union(ref_log_probs)
+
+ # compute values
+ if self.use_critic:
+ with timer("values", timing_raw):
+ values = self.critic_wg.compute_values(batch)
+ batch = batch.union(values)
+
+ with timer("adv", timing_raw):
+ # get token level scores
+ reward_tensor, reward_metrics = ray.get(reward_ref)
+ batch.batch["token_level_scores"] = reward_tensor
+ reward_metrics = {f"reward/{k}": v for k, v in reduce_metrics(reward_metrics).items()}
+ metrics.update(reward_metrics)
+
+ # apply kl penalty if available
+ if not self.config.algorithm.use_kl_loss and self.use_reference_policy:
+ # apply kl penalty to reward
+ batch, kl_metrics = apply_kl_penalty(batch, self.kl_ctrl, self.config.algorithm.kl_penalty)
+ metrics.update(kl_metrics)
+ else:
+ batch.batch["token_level_rewards"] = batch.batch["token_level_scores"]
+
+ # compute advantages, executed on the driver process
+ batch = compute_advantage(
+ batch,
+ adv_estimator=self.config.algorithm.adv_estimator,
+ gamma=self.config.algorithm.gamma,
+ lam=self.config.algorithm.lam,
+ )
+
+ # update critic
+ if self.use_critic:
+ with timer("update_critic", timing_raw):
+ critic_output = self.critic_wg.update_critic(batch)
+
+ critic_metrics = reduce_metrics(critic_output.non_tensor_batch)
+ metrics.update(critic_metrics)
+
+ # update actor
+ if self.config.trainer.critic_warmup <= self.global_step:
+ with timer("update_actor", timing_raw):
+ actor_output = self.actor_rollout_wg.update_actor(batch)
+
+ actor_metrics = reduce_metrics(actor_output.non_tensor_batch)
+ metrics.update(actor_metrics)
+
+ # validate
+ if (
+ self.val_reward_fn is not None
+ and self.config.trainer.val_freq > 0
+ and self.global_step % self.config.trainer.val_freq == 0
+ ):
+ with timer("validation", timing_raw):
+ val_metrics = self._validate()
+
+ metrics.update(val_metrics)
+
+ if self.config.trainer.save_freq > 0 and self.global_step % self.config.trainer.save_freq == 0:
+ with timer("save_checkpoint", timing_raw):
+ self._save_checkpoint()
+
+ # collect metrics
+ num_gpus = self.resource_pool_manager.get_num_gpus()
+ metrics.update(compute_data_metrics(batch=batch, use_critic=self.use_critic))
+ metrics.update(compute_timing_metrics(batch=batch, timing_raw=timing_raw))
+ metrics.update(compute_throughout_metrics(batch=batch, timing_raw=timing_raw, num_gpus=num_gpus))
+
+ self.logger.log(data=metrics, step=self.global_step)
+
+ # perform validation after training
+ if self.val_reward_fn is not None:
+ if (
+ val_metrics is None
+ or self.config.trainer.val_freq <= 0
+ or self.global_step % self.config.trainer.val_freq != 0
+ ):
+ val_metrics = self._validate()
+ self.logger.log(data=val_metrics, step=self.global_step)
+
+ print(f"Final validation metrics: {convert_dict_to_str(val_metrics)}")
+
+ if self.config.trainer.save_freq <= 0 or self.global_step % self.config.trainer.save_freq != 0:
+ self._save_checkpoint()
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..de1a2fcb2b6b796760785a5de9f485eaaa251403
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .checkpoint_manager import CHECKPOINT_TRACKER, remove_obsolete_ckpt
+
+
+__all__ = ["CHECKPOINT_TRACKER", "remove_obsolete_ckpt"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/checkpoint_manager.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/checkpoint_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..749b60cb53c749a03348da4f626ee461741f845e
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/checkpoint_manager.py
@@ -0,0 +1,160 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import random
+import re
+import shutil
+import tempfile
+from abc import ABC, abstractmethod
+from typing import Any, Dict, Optional, Union
+
+import numpy as np
+import torch
+import torch.distributed as dist
+from filelock import FileLock
+from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+from transformers import PreTrainedTokenizer, ProcessorMixin
+
+
+CHECKPOINT_TRACKER = "latest_global_step.txt"
+
+
+class BaseCheckpointManager(ABC):
+ """
+ A checkpoint manager that saves and loads
+ - model
+ - optimizer
+ - lr_scheduler
+ - extra_states
+ in a SPMD way.
+
+ We save
+ - sharded model states and optimizer states
+ - full lr_scheduler states
+ - huggingface tokenizer and config for ckpt merge
+ """
+
+ def __init__(
+ self,
+ model: FSDP,
+ optimizer: torch.optim.Optimizer,
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler,
+ processing_class: Union[PreTrainedTokenizer, ProcessorMixin],
+ ):
+ self.model = model
+ self.optimizer = optimizer
+ self.lr_scheduler = lr_scheduler
+ self.processing_class = processing_class
+
+ assert isinstance(self.model, FSDP)
+ self.rank = dist.get_rank()
+ self.world_size = dist.get_world_size()
+
+ @abstractmethod
+ def load_checkpoint(self, *args, **kwargs):
+ raise NotImplementedError
+
+ @abstractmethod
+ def save_checkpoint(self, *args, **kwargs):
+ raise NotImplementedError
+
+ @staticmethod
+ def local_mkdir(path: str) -> str:
+ if not os.path.isabs(path):
+ working_dir = os.getcwd()
+ path = os.path.join(working_dir, path)
+
+ # Using hash value of path as lock file name to avoid long file name
+ lock_filename = f"ckpt_{hash(path) & 0xFFFFFFFF:08x}.lock"
+ lock_path = os.path.join(tempfile.gettempdir(), lock_filename)
+
+ try:
+ with FileLock(lock_path, timeout=60):
+ os.makedirs(path, exist_ok=True)
+ except Exception as e:
+ print(f"Warning: Failed to acquire lock for {path}: {e}")
+ os.makedirs(path, exist_ok=True) # even if the lock is not acquired, try to create the directory
+
+ return path
+
+ @staticmethod
+ def get_rng_state() -> Dict[str, Any]:
+ rng_state = {
+ "cpu": torch.get_rng_state(),
+ "cuda": torch.cuda.get_rng_state(),
+ "numpy": np.random.get_state(),
+ "random": random.getstate(),
+ }
+ return rng_state
+
+ @staticmethod
+ def load_rng_state(rng_state: Dict[str, Any]):
+ torch.set_rng_state(rng_state["cpu"])
+ torch.cuda.set_rng_state(rng_state["cuda"])
+ np.random.set_state(rng_state["numpy"])
+ random.setstate(rng_state["random"])
+
+
+def find_latest_ckpt_path(path: Optional[str] = None, directory_format: str = "global_step_{}") -> Optional[str]:
+ if path is None:
+ return None
+
+ tracker_file = get_checkpoint_tracker_filename(path)
+ if not os.path.exists(tracker_file):
+ print("Checkpoint tracker file does not exist: %s", tracker_file)
+ return None
+
+ with open(tracker_file, "rb") as f:
+ iteration = int(f.read().decode())
+
+ ckpt_path = os.path.join(path, directory_format.format(iteration))
+ if not os.path.exists(ckpt_path):
+ print("Checkpoint does not exist: %s", ckpt_path)
+ return None
+
+ print("Found checkpoint: %s", ckpt_path)
+ return ckpt_path
+
+
+def get_checkpoint_tracker_filename(root_path: str) -> str:
+ """
+ Tracker file rescords the latest chckpoint during training to restart from.
+ """
+ return os.path.join(root_path, CHECKPOINT_TRACKER)
+
+
+def remove_obsolete_ckpt(path: str, global_step: int, save_limit: int = -1, directory_format: str = "global_step_{}"):
+ """
+ Remove the obsolete checkpoints that exceed the save_limit.
+ """
+ if save_limit <= 0:
+ return
+
+ if not os.path.exists(path):
+ return
+
+ pattern = re.escape(directory_format).replace(r"\{\}", r"(\d+)")
+ ckpt_folders = []
+ for folder in os.listdir(path):
+ if match := re.match(pattern, folder):
+ step = int(match.group(1))
+ if step < global_step:
+ ckpt_folders.append((step, folder))
+
+ ckpt_folders.sort(reverse=True)
+ for _, folder in ckpt_folders[save_limit - 1 :]:
+ folder_path = os.path.join(path, folder)
+ shutil.rmtree(folder_path, ignore_errors=True)
+ print(f"Removed obsolete checkpoint: {folder_path}")
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/fsdp_checkpoint_manager.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/fsdp_checkpoint_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..1318bfeb936c1b7277d9bbd00c8569cbf4ebb831
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/fsdp_checkpoint_manager.py
@@ -0,0 +1,113 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from typing import Optional, Union
+
+import torch
+import torch.distributed as dist
+from torch.distributed.checkpoint.state_dict import StateDictOptions, get_state_dict, set_state_dict
+from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+from transformers import PreTrainedModel, PreTrainedTokenizer, ProcessorMixin
+
+from .checkpoint_manager import BaseCheckpointManager
+
+
+class FSDPCheckpointManager(BaseCheckpointManager):
+ """
+ A checkpoint manager that saves and loads
+ - model
+ - optimizer
+ - lr_scheduler
+ - extra_states
+ in a SPMD way.
+
+ We save
+ - sharded model states and optimizer states
+ - full lr_scheduler states
+ - huggingface tokenizer and config for ckpt merge
+ """
+
+ def __init__(
+ self,
+ model: FSDP,
+ optimizer: torch.optim.Optimizer,
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler,
+ processing_class: Union[PreTrainedTokenizer, ProcessorMixin],
+ ):
+ super().__init__(model, optimizer, lr_scheduler, processing_class)
+
+ def load_checkpoint(self, path: Optional[str] = None):
+ if path is None:
+ return
+
+ # every rank download its own checkpoint
+ model_path = os.path.join(path, f"model_world_size_{self.world_size}_rank_{self.rank}.pt")
+ optim_path = os.path.join(path, f"optim_world_size_{self.world_size}_rank_{self.rank}.pt")
+ extra_path = os.path.join(path, f"extra_state_world_size_{self.world_size}_rank_{self.rank}.pt")
+ print(f"[rank-{self.rank}]: Loading model from {os.path.abspath(model_path)}.")
+ print(f"[rank-{self.rank}]: Loading optimizer from {os.path.abspath(optim_path)}.")
+ print(f"[rank-{self.rank}]: Loading extra_state from {os.path.abspath(extra_path)}.")
+ model_state_dict = torch.load(model_path, weights_only=False)
+ optim_state_dict = torch.load(optim_path, weights_only=False)
+ extra_state_dict = torch.load(extra_path, weights_only=False)
+
+ state_dict_options = StateDictOptions(cpu_offload=True)
+ set_state_dict(
+ model=self.model,
+ optimizers=self.optimizer,
+ model_state_dict=model_state_dict,
+ optim_state_dict=optim_state_dict,
+ options=state_dict_options,
+ )
+ self.lr_scheduler.load_state_dict(extra_state_dict["lr_scheduler"])
+
+ # recover random state
+ if "rng" in extra_state_dict:
+ self.load_rng_state(extra_state_dict["rng"])
+
+ def save_checkpoint(self, path: str):
+ path = self.local_mkdir(path)
+ dist.barrier()
+
+ # every rank will save its own model and optim shard
+ state_dict_options = StateDictOptions(cpu_offload=True)
+ model_state_dict, optim_state_dict = get_state_dict(self.model, self.optimizer, options=state_dict_options)
+ extra_state_dict = {
+ "lr_scheduler": self.lr_scheduler.state_dict(),
+ "rng": self.get_rng_state(),
+ }
+ model_path = os.path.join(path, f"model_world_size_{self.world_size}_rank_{self.rank}.pt")
+ optim_path = os.path.join(path, f"optim_world_size_{self.world_size}_rank_{self.rank}.pt")
+ extra_path = os.path.join(path, f"extra_state_world_size_{self.world_size}_rank_{self.rank}.pt")
+
+ print(f"[rank-{self.rank}]: Saving model to {os.path.abspath(model_path)}.")
+ print(f"[rank-{self.rank}]: Saving optimizer to {os.path.abspath(optim_path)}.")
+ print(f"[rank-{self.rank}]: Saving extra_state to {os.path.abspath(extra_path)}.")
+ torch.save(model_state_dict, model_path)
+ torch.save(optim_state_dict, optim_path)
+ torch.save(extra_state_dict, extra_path)
+
+ # wait for everyone to dump to local
+ dist.barrier()
+
+ if self.rank == 0:
+ hf_path = os.path.join(path, "huggingface")
+ os.makedirs(hf_path, exist_ok=True)
+ assert isinstance(self.model._fsdp_wrapped_module, PreTrainedModel)
+ self.model._fsdp_wrapped_module.config.save_pretrained(hf_path)
+ self.model._fsdp_wrapped_module.generation_config.save_pretrained(hf_path)
+ self.processing_class.save_pretrained(hf_path)
+
+ dist.barrier()
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/dataset.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..d305b054d808499a656d73690bafadccce7c87b7
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/dataset.py
@@ -0,0 +1,235 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+import os
+from collections import defaultdict
+from io import BytesIO
+from typing import Any, Dict, List, Optional, Union
+
+import numpy as np
+import torch
+from datasets import load_dataset
+from jinja2 import Template
+from PIL import Image
+from PIL.Image import Image as ImageObject
+from PIL.Image import Image as PILImage
+from torch.utils.data import Dataset
+from transformers import PreTrainedTokenizer, ProcessorMixin
+from PIL import Image, JpegImagePlugin
+
+from ..models.transformers.qwen2_vl import get_rope_index
+from . import torch_functional as VF
+
+
+def collate_fn(features: List[Dict[str, Any]]) -> Dict[str, Any]:
+ tensors = defaultdict(list)
+ non_tensors = defaultdict(list)
+ for feature in features:
+ for key, value in feature.items():
+ if isinstance(value, torch.Tensor):
+ tensors[key].append(value)
+ else:
+ non_tensors[key].append(value)
+
+ for key, value in tensors.items():
+ tensors[key] = torch.stack(value, dim=0)
+
+ for key, value in non_tensors.items():
+ non_tensors[key] = np.array(value, dtype=object)
+
+ return {**tensors, **non_tensors}
+
+
+
+def process_image(image: Union[Dict[str, Any], ImageObject, str], min_pixels: int, max_pixels: int) -> ImageObject:
+ if isinstance(image, dict):
+ if 'path' in image:
+ image = Image.open(image['path']).copy()
+ else:
+ image = Image.open(BytesIO(image["bytes"]))
+ elif isinstance(image, bytes):
+ image = Image.open(BytesIO(image))
+ elif isinstance(image, list):
+ image = Image.open(image[0]).copy()
+ # image = image[0]
+ elif isinstance(image, PILImage):
+ image = image.copy()
+ else:
+ # image = image
+ raise TypeError(f'Unsupported image type: {type(image)}')
+
+ if (image.width * image.height) > max_pixels:
+ resize_factor = math.sqrt(max_pixels / (image.width * image.height))
+ width, height = int(image.width * resize_factor), int(image.height * resize_factor)
+ image = image.resize((width, height))
+
+ if (image.width * image.height) < min_pixels:
+ resize_factor = math.sqrt(min_pixels / (image.width * image.height))
+ width, height = int(image.width * resize_factor), int(image.height * resize_factor)
+ image = image.resize((width, height))
+
+ if image.mode != "RGB":
+ image = image.convert("RGB")
+
+ return image
+
+def process_video(
+ video: str, min_pixels: Optional[int], max_pixels: Optional[int], video_fps: float, return_fps: bool = False
+) -> Union[list[ImageObject], tuple[list[ImageObject], list[float]]]:
+ vision_info = {"video": video, "min_pixels": min_pixels, "max_pixels": max_pixels, "fps": video_fps}
+ return fetch_video(vision_info, return_video_sample_fps=return_fps)
+
+class RLHFDataset(Dataset):
+ """
+ We assume the dataset contains a column that contains prompts and other information
+ """
+
+ def __init__(
+ self,
+ data_path: str,
+ tokenizer: PreTrainedTokenizer,
+ processor: Optional[ProcessorMixin],
+ prompt_key: str = "prompt",
+ answer_key: str = "answer",
+ image_key: str = "images",
+ max_prompt_length: int = 1024,
+ truncation: str = "error",
+ format_prompt: Optional[str] = None,
+ max_pixels: Optional[int] = None,
+ min_pixels: Optional[int] = None,
+ filter_overlong_prompts: bool = False,
+ ):
+ self.tokenizer = tokenizer
+ self.processor = processor
+ self.prompt_key = prompt_key
+ self.answer_key = answer_key
+ self.image_key = image_key
+ self.max_prompt_length = max_prompt_length
+ self.truncation = truncation
+ self.max_pixels = max_pixels
+ self.min_pixels = min_pixels
+ self.filter_overlong_prompts = filter_overlong_prompts
+
+ if "@" in data_path:
+ data_path, data_split = data_path.split("@")
+ else:
+ data_split = "train"
+
+ if os.path.isdir(data_path):
+ # when we use dataset builder, we should always refer to the train split
+ self.dataset = load_dataset("parquet", data_dir=data_path, split="train")
+ elif os.path.isfile(data_path):
+ self.dataset = load_dataset("parquet", data_files=data_path, split="train")
+ else:
+ # load remote dataset from huggingface hub
+ self.dataset = load_dataset(data_path, split=data_split)
+
+ self.format_prompt = None
+ if format_prompt:
+ with open(format_prompt, encoding="utf-8") as f:
+ self.format_prompt = f.read()
+
+ if self.filter_overlong_prompts:
+ self.dataset = self.dataset.filter(self._filter_overlong_prompts, desc="Filtering overlong prompts")
+
+ def _build_messages(self, example: Dict[str, Any]) -> List[Dict[str, Any]]:
+ prompt_str: str = example[self.prompt_key]
+ if self.format_prompt:
+ format_prompt = Template(self.format_prompt.strip())
+ prompt_str = format_prompt.render(content=prompt_str)
+
+ if self.image_key in example:
+ # https://huggingface.co/docs/transformers/en/tasks/image_text_to_text
+ content_list = []
+ for i, content in enumerate(prompt_str.split("")):
+ if i != 0:
+ content_list.append({"type": "image"})
+
+ if content:
+ content_list.append({"type": "text", "text": content})
+
+ return [{"role": "user", "content": content_list}]
+ else:
+ return [{"role": "user", "content": prompt_str}]
+
+ def _filter_overlong_prompts(self, example: Dict[str, Any]) -> bool:
+ messages = self._build_messages(example)
+ processing_class = self.processor if self.processor is not None else self.tokenizer
+ return (
+ len(processing_class.apply_chat_template(messages, add_generation_prompt=True)) <= self.max_prompt_length
+ )
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, index):
+ example: dict = self.dataset[index]
+ messages = self._build_messages(example)
+
+ if self.image_key in example:
+ prompt = self.processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
+ raw_image_data = example.pop(self.image_key)
+ if not isinstance(raw_image_data, (list, tuple)):
+ raw_image_data = [raw_image_data]
+
+ images = [
+ process_image(image, min_pixels=self.min_pixels, max_pixels=self.max_pixels)
+ for image in raw_image_data
+ ]
+ model_inputs = self.processor(images, [prompt], add_special_tokens=False, return_tensors="pt")
+ input_ids = model_inputs.pop("input_ids")[0]
+ attention_mask = model_inputs.pop("attention_mask")[0]
+ example["multi_modal_data"] = {"image": raw_image_data}
+ else:
+ prompt = self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
+ model_inputs = self.tokenizer([prompt], add_special_tokens=False, return_tensors="pt")
+ input_ids = model_inputs.pop("input_ids")[0]
+ attention_mask = model_inputs.pop("attention_mask")[0]
+
+ if self.processor is not None and self.processor.image_processor.__class__.__name__ == "Qwen2VLImageProcessor":
+ # qwen2vl mrope
+ position_ids = get_rope_index(
+ self.processor,
+ input_ids=input_ids,
+ image_grid_thw=model_inputs.get("image_grid_thw"),
+ attention_mask=attention_mask,
+ ) # (3, seq_length)
+ else:
+ position_ids = torch.clip(attention_mask.cumsum(dim=0) - 1, min=0, max=None) # (seq_length,)
+
+ input_ids, attention_mask, position_ids = VF.postprocess_data(
+ input_ids=input_ids,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ max_length=self.max_prompt_length,
+ pad_token_id=self.tokenizer.pad_token_id,
+ left_pad=True,
+ truncation=self.truncation,
+ )
+ raw_prompt_ids = self.tokenizer.encode(prompt, add_special_tokens=False)
+ if len(raw_prompt_ids) > self.max_prompt_length:
+ if self.truncation == "left":
+ raw_prompt_ids = raw_prompt_ids[-self.max_prompt_length :]
+ elif self.truncation == "right":
+ raw_prompt_ids = raw_prompt_ids[: self.max_prompt_length]
+ elif self.truncation == "error":
+ raise RuntimeError(f"Prompt length {len(raw_prompt_ids)} is longer than {self.max_prompt_length}.")
+
+ example["input_ids"] = input_ids
+ example["attention_mask"] = attention_mask
+ example["position_ids"] = position_ids
+ example["raw_prompt_ids"] = raw_prompt_ids
+ example["ground_truth"] = example.pop(self.answer_key)
+ return example
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/flops_counter.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/flops_counter.py
new file mode 100644
index 0000000000000000000000000000000000000000..5fadf606ff11c05b7d8c1d346aa52bc0cbde7219
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/flops_counter.py
@@ -0,0 +1,138 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import TYPE_CHECKING, List, Tuple
+
+import torch
+
+
+if TYPE_CHECKING:
+ from transformers.models.llama.configuration_llama import LlamaConfig
+
+
+VALID_MODLE_TYPE = {"llama", "mllama", "qwen2", "qwen2_vl", "qwen2_5_vl", "qwen3"}
+
+
+def get_device_flops(unit: str = "T") -> float:
+ def unit_convert(number: float, level: str):
+ units = ["B", "K", "M", "G", "T", "P"]
+ if number <= 0:
+ return number
+
+ ptr = 0
+ while ptr < len(units) and units[ptr] != level:
+ number /= 1000
+ ptr += 1
+
+ return number
+
+ device_name = torch.cuda.get_device_name()
+ flops = float("inf") # INF flops for unkown gpu type
+ if "H100" in device_name or "H800" in device_name:
+ flops = 989e12
+ elif "A100" in device_name or "A800" in device_name:
+ flops = 312e12
+ elif "L40" in device_name:
+ flops = 181.05e12
+ elif "L20" in device_name:
+ flops = 119.5e12
+ elif "H20" in device_name:
+ flops = 148e12
+ elif "910B" in device_name:
+ flops = 354e12
+ flops_unit = unit_convert(flops, unit)
+ return flops_unit
+
+
+class FlopsCounter:
+ """
+ Used to count mfu during training loop
+
+ Example:
+ flops_counter = FlopsCounter(config)
+ flops_achieved, flops_promised = flops_counter.estimate_flops(tokens_list, delta_time)
+ """
+
+ def __init__(self, config: "LlamaConfig"):
+ if config.model_type not in VALID_MODLE_TYPE:
+ print(f"Only support {VALID_MODLE_TYPE}, but got {config.model_type}. MFU will always be zero.")
+
+ self.estimate_func = {
+ "mllama": self._estimate_mllama_flops,
+ "llama": self._estimate_llama_flops,
+ "qwen2": self._estimate_llama_flops,
+ "qwen2_vl": self._estimate_llama_flops,
+ "qwen2_5_vl": self._estimate_llama_flops,
+ }
+ self.config = config
+
+ def _estimate_unknown_flops(self, tokens_sum: int, batch_seqlens: List[int], delta_time: float) -> float:
+ return 0
+
+ def _estimate_llama_flops(self, tokens_sum: int, batch_seqlens: List[int], delta_time: float) -> float:
+ hidden_size = self.config.hidden_size
+ vocab_size = self.config.vocab_size
+ num_hidden_layers = self.config.num_hidden_layers
+ num_key_value_heads = self.config.num_key_value_heads
+ num_attention_heads = self.config.num_attention_heads
+ intermediate_size = self.config.intermediate_size
+
+ head_dim = hidden_size // num_attention_heads
+ q_size = num_attention_heads * head_dim
+ k_size = num_key_value_heads * head_dim
+ v_size = num_key_value_heads * head_dim
+
+ # non-attn per layer parm
+ # Qwen2/LLama use SwiGelu, gate, having up and down linear layer in mlp
+ mlp_N = hidden_size * intermediate_size * 3
+ attn_linear_N = hidden_size * (q_size + k_size + v_size + num_attention_heads * head_dim)
+ emd_and_lm_head_N = vocab_size * hidden_size * 2
+ # non-attn all_layer parm
+ dense_N = (mlp_N + attn_linear_N) * num_hidden_layers + emd_and_lm_head_N
+ # non-attn all_layer & all_token fwd & bwd flops
+ dense_N_flops = 6 * dense_N * tokens_sum
+
+ # attn all_layer & all_token fwd & bwd flops
+ seqlen_square_sum = 0
+ for seqlen in batch_seqlens:
+ seqlen_square_sum += seqlen * seqlen
+
+ attn_qkv_flops = 12 * seqlen_square_sum * head_dim * num_attention_heads * num_hidden_layers
+
+ # all_layer & all_token fwd & bwd flops
+ flops_all_token = dense_N_flops + attn_qkv_flops
+ flops_achieved = flops_all_token * (1.0 / delta_time) / 1e12
+ return flops_achieved
+
+ def _estimate_mllama_flops(self, tokens_sum: int, batch_seqlens: List[int], delta_time: float) -> float:
+
+ return 0
+
+ def estimate_flops(self, batch_seqlens: List[int], delta_time: float) -> Tuple[float, float]:
+ """
+ Estimate the FLOPS based on the number of valid tokens in the current batch and the time taken.
+
+ Args:
+ batch_seqlens (List[int]): A list where each element represents the number of valid tokens in the current batch.
+ delta_time (float): The time taken to process the batch, in seconds.
+
+ Returns:
+ estimated_flops (float): The estimated FLOPS based on the input tokens and time.
+ promised_flops (float): The expected FLOPS of the current device.
+ """
+ tokens_sum = sum(batch_seqlens)
+ func = self.estimate_func.get(self.config.model_type, self._estimate_unknown_flops)
+ estimated_flops = func(tokens_sum, batch_seqlens, delta_time)
+ promised_flops = get_device_flops()
+ return estimated_flops, promised_flops
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/fsdp_utils.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/fsdp_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ca563a755160705f96f3e3176af6b16ce510be6
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/fsdp_utils.py
@@ -0,0 +1,140 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import gc
+from collections import defaultdict
+from functools import partial
+from typing import Callable, Union
+
+import torch
+from torch import nn
+from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+from torch.distributed.fsdp._runtime_utils import _lazy_init
+from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
+from torch.optim import Optimizer
+from transformers import PreTrainedModel
+from transformers.trainer_pt_utils import get_module_class_from_name
+
+
+def get_init_fn(model: nn.Module, device: Union[str, torch.device]) -> Callable[[nn.Module], None]:
+ param_occurrence = defaultdict(int)
+ for _, param in model.named_parameters(remove_duplicate=False):
+ param_occurrence[param] += 1
+
+ duplicated_params = {param for param in param_occurrence.keys() if param_occurrence[param] > 1}
+ materialized_params = {}
+
+ def init_fn(module: nn.Module):
+ for name, param in module.named_parameters(recurse=False):
+ if param in duplicated_params:
+ module._parameters[name] = materialized_params.setdefault(
+ param, nn.Parameter(torch.empty_like(param.data, device=device), requires_grad=param.requires_grad)
+ )
+ else:
+ module._parameters[name] = nn.Parameter(
+ torch.empty_like(param.data, device=device), requires_grad=param.requires_grad
+ )
+
+ return init_fn
+
+
+def get_fsdp_wrap_policy(model: PreTrainedModel):
+ """Get FSDP wrap policy for the model.
+
+ Args:
+ module: The module to get wrap policy for
+ """
+ transformer_cls_to_wrap = set()
+ for module in model._no_split_modules:
+ transformer_cls = get_module_class_from_name(model, module)
+ if transformer_cls is None:
+ raise Exception(f"Cannot find {module} in pretrained model.")
+ else:
+ transformer_cls_to_wrap.add(transformer_cls)
+
+ return partial(transformer_auto_wrap_policy, transformer_layer_cls=transformer_cls_to_wrap)
+
+
+@torch.no_grad()
+def offload_fsdp_model(model: FSDP, empty_cache: bool = True):
+ # lazy init FSDP model
+ _lazy_init(model, model)
+ assert model._is_root, "Only support root model offloading to CPU"
+ for handle in model._all_handles:
+ if handle._offload_params:
+ continue
+
+ flat_param = handle.flat_param
+ assert (
+ flat_param.data.data_ptr() == flat_param._local_shard.data_ptr()
+ and id(flat_param.data) != id(flat_param._local_shard)
+ and flat_param.data.size() == flat_param._local_shard.size()
+ )
+ handle.flat_param_to("cpu", non_blocking=True)
+ # the following still keeps id(._local_shard) != id(.data)
+ flat_param._local_shard = flat_param.data
+ assert id(flat_param._local_shard) != id(flat_param.data)
+
+ if empty_cache:
+ torch.cuda.empty_cache()
+
+
+@torch.no_grad()
+def load_fsdp_model(model: FSDP, empty_cache: bool = True):
+ # lazy init FSDP model
+ _lazy_init(model, model)
+ assert model._is_root, "Only support root model loading to GPU"
+ for handle in model._all_handles:
+ if handle._offload_params:
+ continue
+
+ flat_param = handle.flat_param
+ handle.flat_param_to("cuda", non_blocking=True)
+ # the following still keeps id(._local_shard) != id(.data)
+ flat_param._local_shard = flat_param.data
+
+ if empty_cache:
+ gc.collect()
+
+
+@torch.no_grad()
+def offload_fsdp_optimizer(optimizer: Optimizer, empty_cache: bool = True):
+ if not optimizer.state:
+ return
+
+ for param_group in optimizer.param_groups:
+ for param in param_group["params"]:
+ state = optimizer.state[param]
+ for key, value in state.items():
+ if isinstance(value, torch.Tensor):
+ state[key] = value.to("cpu", non_blocking=True)
+
+ if empty_cache:
+ torch.cuda.empty_cache()
+
+
+@torch.no_grad()
+def load_fsdp_optimizer(optimizer: Optimizer, empty_cache: bool = True):
+ if not optimizer.state:
+ return
+
+ for param_group in optimizer.param_groups:
+ for param in param_group["params"]:
+ state = optimizer.state[param]
+ for key, value in state.items():
+ if isinstance(value, torch.Tensor):
+ state[key] = value.to("cuda", non_blocking=True)
+
+ if empty_cache:
+ gc.collect()
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..557c47750000363fc62c8b9fea11c9299b3d901f
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .logger import Tracker
+
+
+__all__ = ["Tracker"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/gen_logger.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/gen_logger.py
new file mode 100644
index 0000000000000000000000000000000000000000..b62cde6cf52af087585a92441db0fcfdc27bccb0
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/gen_logger.py
@@ -0,0 +1,102 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import List, Tuple
+
+from ..py_functional import is_package_available
+
+
+if is_package_available("wandb"):
+ import wandb # type: ignore
+
+
+if is_package_available("swanlab"):
+ import swanlab # type: ignore
+
+
+@dataclass
+class GenerationLogger(ABC):
+ @abstractmethod
+ def log(self, samples: List[Tuple[str, str, str, float]], step: int) -> None: ...
+
+
+@dataclass
+class ConsoleGenerationLogger(GenerationLogger):
+ def log(self, samples: List[Tuple[str, str, str, float]], step: int) -> None:
+ for inp, out, lab, score in samples:
+ print(f"[prompt] {inp}\n[output] {out}\n[ground_truth] {lab}\n[score] {score}\n")
+
+
+@dataclass
+class WandbGenerationLogger(GenerationLogger):
+ def log(self, samples: List[Tuple[str, str, str, float]], step: int) -> None:
+ # Create column names for all samples
+ columns = ["step"] + sum(
+ [[f"input_{i + 1}", f"output_{i + 1}", f"label_{i + 1}", f"score_{i + 1}"] for i in range(len(samples))],
+ [],
+ )
+
+ if not hasattr(self, "validation_table"):
+ # Initialize the table on first call
+ self.validation_table = wandb.Table(columns=columns)
+
+ # Create a new table with same columns and existing data
+ # Workaround for https://github.com/wandb/wandb/issues/2981#issuecomment-1997445737
+ new_table = wandb.Table(columns=columns, data=self.validation_table.data)
+
+ # Add new row with all data
+ row_data = [step]
+ for sample in samples:
+ row_data.extend(sample)
+
+ new_table.add_data(*row_data)
+ wandb.log({"val/generations": new_table}, step=step)
+ self.validation_table = new_table
+
+
+@dataclass
+class SwanlabGenerationLogger(GenerationLogger):
+ def log(self, samples: List[Tuple[str, str, str, float]], step: int) -> None:
+ swanlab_text_list = []
+ for i, sample in enumerate(samples):
+ row_text = "\n\n---\n\n".join(
+ (f"input: {sample[0]}", f"output: {sample[1]}", f"label: {sample[2]}", f"score: {sample[3]}")
+ )
+ swanlab_text_list.append(swanlab.Text(row_text, caption=f"sample {i + 1}"))
+
+ swanlab.log({"val/generations": swanlab_text_list}, step=step)
+
+
+GEN_LOGGERS = {
+ "console": ConsoleGenerationLogger,
+ "wandb": WandbGenerationLogger,
+ "swanlab": SwanlabGenerationLogger,
+}
+
+
+@dataclass
+class AggregateGenerationsLogger:
+ def __init__(self, loggers: List[str]):
+ self.loggers: List[GenerationLogger] = []
+
+ for logger in loggers:
+ if logger in GEN_LOGGERS:
+ self.loggers.append(GEN_LOGGERS[logger]())
+
+ def log(self, samples: List[Tuple[str, str, str, float]], step: int) -> None:
+ for logger in self.loggers:
+ logger.log(samples, step)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/logger.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/logger.py
new file mode 100644
index 0000000000000000000000000000000000000000..a29fb5051205f1ef02d68b0810b4cd64a33b985e
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/logger/logger.py
@@ -0,0 +1,165 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+A unified tracking interface that supports logging data to different backend
+"""
+
+import os
+from abc import ABC, abstractmethod
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+import torch
+
+from ..py_functional import convert_dict_to_str, flatten_dict, is_package_available, unflatten_dict
+from .gen_logger import AggregateGenerationsLogger
+
+
+if is_package_available("mlflow"):
+ import mlflow # type: ignore
+
+
+if is_package_available("tensorboard"):
+ from torch.utils.tensorboard import SummaryWriter
+
+
+if is_package_available("wandb"):
+ import wandb # type: ignore
+
+
+if is_package_available("swanlab"):
+ import swanlab # type: ignore
+
+
+class Logger(ABC):
+ @abstractmethod
+ def __init__(self, config: Dict[str, Any]) -> None: ...
+
+ @abstractmethod
+ def log(self, data: Dict[str, Any], step: int) -> None: ...
+
+ def finish(self) -> None:
+ pass
+
+
+class ConsoleLogger(Logger):
+ def __init__(self, config: Dict[str, Any]) -> None:
+ print("Config\n" + convert_dict_to_str(config))
+
+ def log(self, data: Dict[str, Any], step: int) -> None:
+ print(f"Step {step}\n" + convert_dict_to_str(unflatten_dict(data)))
+
+
+class MlflowLogger(Logger):
+ def __init__(self, config: Dict[str, Any]) -> None:
+ mlflow.start_run(run_name=config["trainer"]["experiment_name"])
+ mlflow.log_params(flatten_dict(config))
+
+ def log(self, data: Dict[str, Any], step: int) -> None:
+ mlflow.log_metrics(metrics=data, step=step)
+
+
+class SwanlabLogger(Logger):
+ def __init__(self, config: Dict[str, Any]) -> None:
+ swanlab_key = os.getenv("SWANLAB_API_KEY")
+ swanlab_dir = os.getenv("SWANLAB_DIR", "swanlab_log")
+ swanlab_mode = os.getenv("SWANLAB_MODE", "cloud")
+ if swanlab_key:
+ swanlab.login(swanlab_key)
+
+ swanlab.init(
+ project=config["trainer"]["project_name"],
+ experiment_name=config["trainer"]["experiment_name"],
+ config={"UPPERFRAMEWORK": "EasyR1", "FRAMEWORK": "veRL", **config},
+ logdir=swanlab_dir,
+ mode=swanlab_mode,
+ )
+
+ def log(self, data: Dict[str, Any], step: int) -> None:
+ swanlab.log(data=data, step=step)
+
+ def finish(self) -> None:
+ swanlab.finish()
+
+
+class TensorBoardLogger(Logger):
+ def __init__(self, config: Dict[str, Any]) -> None:
+ tensorboard_dir = os.getenv("TENSORBOARD_DIR", "tensorboard_log")
+ os.makedirs(tensorboard_dir, exist_ok=True)
+ print(f"Saving tensorboard log to {tensorboard_dir}.")
+ self.writer = SummaryWriter(tensorboard_dir)
+ config_dict = {}
+ for key, value in flatten_dict(config).items():
+ if isinstance(value, (int, float, str, bool, torch.Tensor)):
+ config_dict[key] = value
+ else:
+ config_dict[key] = str(value)
+
+ self.writer.add_hparams(hparam_dict=config_dict, metric_dict={"placeholder": 0})
+
+ def log(self, data: Dict[str, Any], step: int) -> None:
+ for key, value in data.items():
+ self.writer.add_scalar(key, value, step)
+
+ def finish(self):
+ self.writer.close()
+
+
+class WandbLogger(Logger):
+ def __init__(self, config: Dict[str, Any]) -> None:
+ wandb.init(
+ project=config["trainer"]["project_name"],
+ name=config["trainer"]["experiment_name"],
+ config=config,
+ )
+
+ def log(self, data: Dict[str, Any], step: int) -> None:
+ wandb.log(data=data, step=step)
+
+ def finish(self) -> None:
+ wandb.finish()
+
+
+LOGGERS = {
+ "console": ConsoleLogger,
+ "mlflow": MlflowLogger,
+ "swanlab": SwanlabLogger,
+ "tensorboard": TensorBoardLogger,
+ "wandb": WandbLogger,
+}
+
+
+class Tracker:
+ def __init__(self, loggers: Union[str, List[str]] = "console", config: Optional[Dict[str, Any]] = None):
+ if isinstance(loggers, str):
+ loggers = [loggers]
+
+ self.loggers: List[Logger] = []
+ for logger in loggers:
+ if logger not in LOGGERS:
+ raise ValueError(f"{logger} is not supported.")
+
+ self.loggers.append(LOGGERS[logger](config))
+
+ self.gen_logger = AggregateGenerationsLogger(loggers)
+
+ def log(self, data: Dict[str, Any], step: int) -> None:
+ for logger in self.loggers:
+ logger.log(data=data, step=step)
+
+ def log_generation(self, samples: List[Tuple[str, str, str, float]], step: int) -> None:
+ self.gen_logger.log(samples, step)
+
+ def __del__(self):
+ for logger in self.loggers:
+ logger.finish()
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/model_utils.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/model_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..71d4fe2c4e9c481972242d5b20cf82e669f68e30
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/model_utils.py
@@ -0,0 +1,73 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Utilities to create common models
+"""
+
+from functools import lru_cache
+from typing import Optional, Tuple
+
+import torch
+import torch.distributed as dist
+from torch import nn
+
+
+@lru_cache
+def is_rank0() -> int:
+ return (not dist.is_initialized()) or (dist.get_rank() == 0)
+
+
+def print_gpu_memory_usage(prefix: str = "GPU memory usage") -> None:
+ """Report the current GPU VRAM usage."""
+ if is_rank0():
+ free_mem, total_mem = torch.cuda.mem_get_info()
+ print(f"{prefix}: {(total_mem - free_mem) / (1024**3):.2f} GB / {total_mem / (1024**3):.2f} GB.")
+
+
+def _get_model_size(model: nn.Module, scale: str = "auto") -> Tuple[float, str]:
+ """Compute the model size."""
+ n_params = sum(p.numel() for p in model.parameters())
+
+ if scale == "auto":
+ if n_params > 1e9:
+ scale = "B"
+ elif n_params > 1e6:
+ scale = "M"
+ elif n_params > 1e3:
+ scale = "K"
+ else:
+ scale = ""
+
+ if scale == "B":
+ n_params = n_params / 1e9
+ elif scale == "M":
+ n_params = n_params / 1e6
+ elif scale == "K":
+ n_params = n_params / 1e3
+ elif scale == "":
+ pass
+ else:
+ raise NotImplementedError(f"Unknown scale {scale}.")
+
+ return n_params, scale
+
+
+def print_model_size(model: nn.Module, name: Optional[str] = None) -> None:
+ """Print the model size."""
+ if is_rank0():
+ n_params, scale = _get_model_size(model, scale="auto")
+ if name is None:
+ name = model.__class__.__name__
+
+ print(f"{name} contains {n_params:.2f}{scale} parameters.")
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/py_functional.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/py_functional.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a9ed3cce66e5a818a827d080f8d90a968d15081
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/py_functional.py
@@ -0,0 +1,113 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Contain small python utility functions
+"""
+
+import importlib.util
+import re
+from contextlib import contextmanager
+from functools import lru_cache
+from typing import Any, Dict, List, Union
+
+import numpy as np
+import yaml
+from codetiming import Timer
+from yaml import Dumper
+
+
+def is_sci_notation(number: float) -> bool:
+ pattern = re.compile(r"^[+-]?\d+(\.\d*)?[eE][+-]?\d+$")
+ return bool(pattern.match(str(number)))
+
+
+def float_representer(dumper: Dumper, number: Union[float, np.float32, np.float64]):
+ if is_sci_notation(number):
+ value = str(number)
+ if "." not in value and "e" in value:
+ value = value.replace("e", ".0e", 1)
+ else:
+ value = str(round(number, 3))
+
+ return dumper.represent_scalar("tag:yaml.org,2002:float", value)
+
+
+yaml.add_representer(float, float_representer)
+yaml.add_representer(np.float32, float_representer)
+yaml.add_representer(np.float64, float_representer)
+
+
+@lru_cache
+def is_package_available(name: str) -> bool:
+ return importlib.util.find_spec(name) is not None
+
+
+def union_two_dict(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
+ """Union two dict. Will throw an error if there is an item not the same object with the same key."""
+ for key in dict2.keys():
+ if key in dict1:
+ assert dict1[key] == dict2[key], f"{key} in dict1 and dict2 are not the same object"
+
+ dict1[key] = dict2[key]
+
+ return dict1
+
+
+def append_to_dict(data: Dict[str, List[Any]], new_data: Dict[str, Any]) -> None:
+ """Append dict to a dict of list."""
+ for key, val in new_data.items():
+ if key not in data:
+ data[key] = []
+
+ data[key].append(val)
+
+
+def unflatten_dict(data: Dict[str, Any], sep: str = "/") -> Dict[str, Any]:
+ unflattened = {}
+ for key, value in data.items():
+ pieces = key.split(sep)
+ pointer = unflattened
+ for piece in pieces[:-1]:
+ if piece not in pointer:
+ pointer[piece] = {}
+
+ pointer = pointer[piece]
+
+ pointer[pieces[-1]] = value
+
+ return unflattened
+
+
+def flatten_dict(data: Dict[str, Any], parent_key: str = "", sep: str = "/") -> Dict[str, Any]:
+ flattened = {}
+ for key, value in data.items():
+ new_key = parent_key + sep + key if parent_key else key
+ if isinstance(value, dict):
+ flattened.update(flatten_dict(value, new_key, sep=sep))
+ else:
+ flattened[new_key] = value
+
+ return flattened
+
+
+def convert_dict_to_str(data: Dict[str, Any]) -> str:
+ return yaml.dump(data, indent=2)
+
+
+@contextmanager
+def timer(name: str, timing_raw: Dict[str, float]):
+ with Timer(name=name, logger=None) as timer:
+ yield
+
+ timing_raw[name] = timer.last
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/seqlen_balancing.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/seqlen_balancing.py
new file mode 100644
index 0000000000000000000000000000000000000000..5889784dad4a53ed52c2c372f175685ac49aafe0
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/seqlen_balancing.py
@@ -0,0 +1,264 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+import heapq
+from typing import List, Tuple
+
+import torch
+from tensordict import TensorDict
+from torch import distributed as dist
+
+
+class Set:
+ def __init__(self) -> None:
+ self.sum = 0
+ self.items = []
+
+ def add(self, idx: int, val: int):
+ self.items.append((idx, val))
+ self.sum += val
+
+ def merge(self, other):
+ for idx, val in other.items:
+ self.items.append((idx, val))
+ self.sum += val
+
+ def __lt__(self, other):
+ if self.sum != other.sum:
+ return self.sum < other.sum
+ if len(self.items) != len(other.items):
+ return len(self.items) < len(other.items)
+ return self.items < other.items
+
+
+class State:
+ def __init__(self, items: List[Tuple[int, int]], k: int) -> None:
+ self.k = k
+ # sets should always be decreasing order
+ self.sets = [Set() for _ in range(k)]
+ assert len(items) in [1, k], f"{len(items)} not in [1, {k}]"
+ for i, (idx, seqlen) in enumerate(items):
+ self.sets[i].add(idx=idx, val=seqlen)
+ self.sets = sorted(self.sets, reverse=True)
+
+ def get_partitions(self):
+ partitions = []
+ for i in range(len(self.sets)):
+ cur_partition = []
+ for idx, _ in self.sets[i].items:
+ cur_partition.append(idx)
+ partitions.append(cur_partition)
+ return partitions
+
+ def merge(self, other):
+ for i in range(self.k):
+ self.sets[i].merge(other.sets[self.k - 1 - i])
+ self.sets = sorted(self.sets, reverse=True)
+
+ @property
+ def spread(self) -> int:
+ return self.sets[0].sum - self.sets[-1].sum
+
+ def __lt__(self, other):
+ # least heap, let the state with largest spread to be popped first,
+ # if the spread is the same, let the state who has the largest set
+ # to be popped first.
+ if self.spread != other.spread:
+ return self.spread > other.spread
+ return self.sets[0] > other.sets[0]
+
+ def __repr__(self) -> str:
+ repr_str = "["
+ for i in range(self.k):
+ if i > 0:
+ repr_str += ","
+ repr_str += "{"
+ for j, (_, seqlen) in enumerate(self.sets[i].items):
+ if j > 0:
+ repr_str += ","
+ repr_str += str(seqlen)
+ repr_str += "}"
+ repr_str += "]"
+ return repr_str
+
+
+def karmarkar_karp(seqlen_list: List[int], k_partitions: int, equal_size: bool):
+ # see: https://en.wikipedia.org/wiki/Largest_differencing_method
+ sorted_seqlen_list = sorted([(seqlen, i) for i, seqlen in enumerate(seqlen_list)])
+ states_pq: List[State] = []
+ if equal_size:
+ assert len(seqlen_list) % k_partitions == 0, f"{len(seqlen_list)} % {k_partitions} != 0"
+ for offset in range(0, len(sorted_seqlen_list), k_partitions):
+ items = []
+ for i in range(k_partitions):
+ seqlen, idx = sorted_seqlen_list[offset + i]
+ items.append((idx, seqlen))
+ heapq.heappush(states_pq, State(items=items, k=k_partitions))
+ else:
+ for seqlen, idx in sorted_seqlen_list:
+ heapq.heappush(states_pq, State(items=[(idx, seqlen)], k=k_partitions))
+
+ while len(states_pq) > 1:
+ state0 = heapq.heappop(states_pq)
+ state1 = heapq.heappop(states_pq)
+ # merge states
+ state0.merge(state1)
+ heapq.heappush(states_pq, state0)
+
+ final_state = states_pq[0]
+ partitions = final_state.get_partitions()
+ if equal_size:
+ for i, partition in enumerate(partitions):
+ assert len(partition) * k_partitions == len(seqlen_list), (
+ f"{len(partition)} * {k_partitions} != {len(seqlen_list)}"
+ )
+ return partitions
+
+
+def greedy_partition(seqlen_list: List[int], k_partitions: int, equal_size: bool):
+ bias = sum(seqlen_list) + 1 if equal_size else 0
+ sorted_seqlen = [(seqlen + bias, i) for i, seqlen in enumerate(seqlen_list)]
+ partitions = [[] for _ in range(k_partitions)]
+ partition_sums = [0 for _ in range(k_partitions)]
+ for seqlen, i in sorted_seqlen:
+ min_idx = None
+ for j in range(k_partitions):
+ if min_idx is None or partition_sums[j] < partition_sums[min_idx]:
+ min_idx = j
+ partitions[min_idx].append(i)
+ partition_sums[min_idx] += seqlen
+ if equal_size:
+ for i, partition in enumerate(partitions):
+ assert len(partition) * k_partitions == len(seqlen_list), (
+ f"{len(partition)} * {k_partitions} != {len(seqlen_list)}"
+ )
+ return partitions
+
+
+def get_seqlen_balanced_partitions(seqlen_list: List[int], k_partitions: int, equal_size: bool):
+ """get order of seq lengths to make partitions balanced, this is
+ used in balacing sum of seqlength across dp ranks and microbatches
+ Parameters:
+ seqlen_list (List[int]):
+ seq lengths of each items
+ k_partitions (int):
+ resulting number of partitions
+ equal_size (bool):
+ if True, number of items in each partitions must be equal.
+ if False, only consider balancing the sum, each partition can have
+ variable number of items
+ Returns:
+ partitions (List[List[int]]):
+ return k_partitions list containing the index of items.
+ """
+ assert len(seqlen_list) >= k_partitions, f"number of items:[{len(seqlen_list)}] < k_partitions:[{k_partitions}]"
+
+ def _check_and_sort_partitions(partitions):
+ assert len(partitions) == k_partitions, f"{len(partitions)} != {k_partitions}"
+ seen_idx = set()
+ sorted_partitions = [None] * k_partitions
+ for i, partition in enumerate(partitions):
+ assert len(partition) > 0, f"the {i}-th partition is empty"
+ for idx in partition:
+ seen_idx.add(idx)
+ sorted_partitions[i] = sorted(partition)
+ assert seen_idx == set(range(len(seqlen_list)))
+ return sorted_partitions
+
+ partitions = karmarkar_karp(seqlen_list=seqlen_list, k_partitions=k_partitions, equal_size=equal_size)
+ return _check_and_sort_partitions(partitions)
+
+
+def log_seqlen_unbalance(seqlen_list: List[int], partitions: List[List[int]], prefix):
+ # add some metrics of seqlen sum on dp ranks
+ k_partition = len(partitions)
+ # assert len(seqlen_list) % k_partition == 0
+ batch_size = len(seqlen_list) // k_partition
+ min_sum_seqlen = None
+ max_sum_seqlen = None
+ total_sum_seqlen = 0
+ for offset in range(0, len(seqlen_list), batch_size):
+ cur_sum_seqlen = sum(seqlen_list[offset : offset + batch_size])
+ if min_sum_seqlen is None or cur_sum_seqlen < min_sum_seqlen:
+ min_sum_seqlen = cur_sum_seqlen
+ if max_sum_seqlen is None or cur_sum_seqlen > max_sum_seqlen:
+ max_sum_seqlen = cur_sum_seqlen
+ total_sum_seqlen += cur_sum_seqlen
+
+ balanced_sum_seqlen_list = []
+ for partition in partitions:
+ cur_sum_seqlen_balanced = sum([seqlen_list[i] for i in partition])
+ balanced_sum_seqlen_list.append(cur_sum_seqlen_balanced)
+ # print("balanced_sum_seqlen_list: ", balanced_sum_seqlen_list)
+ min_sum_seqlen_balanced = min(balanced_sum_seqlen_list)
+ max_sum_seqlen_balanced = max(balanced_sum_seqlen_list)
+
+ return {
+ f"{prefix}/min": min_sum_seqlen,
+ f"{prefix}/max": max_sum_seqlen,
+ f"{prefix}/minmax_diff": max_sum_seqlen - min_sum_seqlen,
+ f"{prefix}/balanced_min": min_sum_seqlen_balanced,
+ f"{prefix}/balanced_max": max_sum_seqlen_balanced,
+ f"{prefix}/mean": total_sum_seqlen / len(partitions),
+ }
+
+
+def ceildiv(a, b):
+ return -(a // -b)
+
+
+def rearrange_micro_batches(batch: TensorDict, max_token_len, dp_group=None):
+ """Split the batch into a list of micro_batches, where the max_token_len is smaller than max_token_len
+ and the number of valid tokens in each micro batch is well balanced.
+ """
+ # this is per local micro_bsz
+ max_seq_len = batch["attention_mask"].shape[-1]
+ assert max_token_len >= max_seq_len, (
+ f"max_token_len must be greater than the sequence length. Got {max_token_len=} and {max_seq_len=}"
+ )
+
+ seq_len_effective: torch.Tensor = batch["attention_mask"].sum(dim=1)
+ total_seqlen = seq_len_effective.sum().item()
+ num_micro_batches = ceildiv(total_seqlen, max_token_len)
+ if dist.is_initialized():
+ num_micro_batches = torch.tensor([num_micro_batches], device="cuda")
+ dist.all_reduce(num_micro_batches, op=dist.ReduceOp.MAX, group=dp_group)
+ num_micro_batches = num_micro_batches.cpu().item()
+
+ seq_len_effective = seq_len_effective.tolist()
+ assert num_micro_batches <= len(seq_len_effective)
+
+ micro_bsz_idx = get_seqlen_balanced_partitions(seq_len_effective, num_micro_batches, equal_size=False)
+
+ micro_batches = []
+
+ for partition in micro_bsz_idx:
+ curr_micro_batch = []
+ for idx in partition:
+ curr_micro_batch.append(batch[idx : idx + 1])
+ curr_micro_batch = torch.cat(curr_micro_batch)
+
+ micro_batches.append(curr_micro_batch)
+
+ return micro_batches, micro_bsz_idx
+
+
+def get_reverse_idx(idx_map):
+ reverse_idx_map = copy.deepcopy(idx_map)
+
+ for i, idx in enumerate(idx_map):
+ reverse_idx_map[idx] = i
+
+ return reverse_idx_map
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/tokenizer.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/tokenizer.py
new file mode 100644
index 0000000000000000000000000000000000000000..b339e2af9ff19130cc7d17be5add82d5940ef9dc
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/tokenizer.py
@@ -0,0 +1,51 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Utils for tokenization."""
+
+from typing import Optional
+
+from transformers import AutoProcessor, AutoTokenizer, PreTrainedTokenizer, ProcessorMixin
+
+
+def get_tokenizer(model_path: str, override_chat_template: Optional[str] = None, **kwargs) -> PreTrainedTokenizer:
+ """Create a huggingface pretrained tokenizer."""
+ tokenizer = AutoTokenizer.from_pretrained(model_path, **kwargs)
+ if override_chat_template is not None:
+ tokenizer.chat_template = override_chat_template
+
+ if tokenizer.bos_token == "" and tokenizer.eos_token == "":
+ # the EOS token in gemma2 & gemma3 is ambiguious, which may worsen RL performance.
+ # https://huggingface.co/google/gemma-2-2b-it/commit/17a01657f5c87135bcdd0ec7abb4b2dece04408a
+ print("Found gemma model. Set eos_token and eos_token_id to and 107.")
+ tokenizer.eos_token = ""
+
+ if tokenizer.pad_token_id is None:
+ print("Pad token is None. Set it to eos_token.")
+ tokenizer.pad_token = tokenizer.eos_token
+
+ return tokenizer
+
+
+def get_processor(model_path: str, override_chat_template: Optional[str] = None, **kwargs) -> Optional[ProcessorMixin]:
+ """Create a huggingface pretrained processor."""
+ processor = AutoProcessor.from_pretrained(model_path, **kwargs)
+ if override_chat_template is not None:
+ processor.chat_template = override_chat_template
+
+ # Avoid load tokenizer, see:
+ # https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/models/auto/processing_auto.py#L344
+ if processor is not None and "Processor" not in processor.__class__.__name__:
+ processor = None
+
+ return processor
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/torch_dtypes.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/torch_dtypes.py
new file mode 100644
index 0000000000000000000000000000000000000000..e50c5c3b7192fa3c03c2cd32f3b20b031f45166a
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/torch_dtypes.py
@@ -0,0 +1,58 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+
+
+HALF_LIST = ["fp16", "float16"]
+FLOAT_LIST = ["fp32", "float32"]
+BFLOAT_LIST = ["bf16", "bfloat16"]
+
+
+class PrecisionType:
+ """Type of precision used."""
+
+ @staticmethod
+ def is_fp16(precision: str) -> bool:
+ return precision in HALF_LIST
+
+ @staticmethod
+ def is_fp32(precision: str) -> bool:
+ return precision in FLOAT_LIST
+
+ @staticmethod
+ def is_bf16(precision: str) -> bool:
+ return precision in BFLOAT_LIST
+
+ @staticmethod
+ def to_dtype(precision: str) -> torch.dtype:
+ if precision in HALF_LIST:
+ return torch.float16
+ elif precision in FLOAT_LIST:
+ return torch.float32
+ elif precision in BFLOAT_LIST:
+ return torch.bfloat16
+ else:
+ raise RuntimeError(f"Unexpected precision: {precision}")
+
+ @staticmethod
+ def to_str(precision: torch.dtype) -> str:
+ if precision == torch.float16:
+ return "float16"
+ elif precision == torch.float32:
+ return "float32"
+ elif precision == torch.bfloat16:
+ return "bfloat16"
+ else:
+ raise RuntimeError(f"Unexpected precision: {precision}")
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/torch_functional.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/torch_functional.py
new file mode 100644
index 0000000000000000000000000000000000000000..8392e545d318962039effa8ef6f8c7b96c4be0ed
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/torch_functional.py
@@ -0,0 +1,340 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+# Copyright Meta Platforms, Inc. and affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Contain small torch utilities
+"""
+
+from typing import List, Literal, Optional, Tuple, Union
+
+import torch
+import torch.distributed
+import torch.nn.functional as F
+from torch.optim.lr_scheduler import LambdaLR
+
+from .torch_dtypes import PrecisionType
+
+
+try:
+ from flash_attn.ops.triton.cross_entropy import cross_entropy_loss
+
+ FLAH_ATTN_CROSS_ENTROPY_LOSS_AVAILABLE = True
+except ImportError:
+ FLAH_ATTN_CROSS_ENTROPY_LOSS_AVAILABLE = False
+
+
+@torch.compiler.disable()
+def log_probs_from_logits_flash_attn(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
+ output = cross_entropy_loss(logits, labels, inplace_backward=True)
+ if not isinstance(output, tuple):
+ raise ValueError(
+ "please make sure flash-attn>=2.4.3 where cross_entropy_loss returns Tuple[losses, z_losses]."
+ )
+
+ return -output[0]
+
+
+def log_probs_from_logits(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
+ """Compute log probs on the label ids given logits.
+
+ We may use torch compile to speed up computing.
+
+ Args:
+ logits (torch.Tensor): logits of the model, shape (batch_size, seqlen, vocab_size)
+ labels (torch.Tensor): labels of the model, shape (batch_size, seqlen)
+
+ Returns:
+ torch.Tensor: log probs of the labels, shape (batch_size, seqlen)
+ """
+ batch_dim = logits.shape[:-1]
+ vocab_dim = logits.shape[-1]
+ logits = logits.contiguous().view(-1, vocab_dim)
+ labels = labels.contiguous().view(-1)
+ if FLAH_ATTN_CROSS_ENTROPY_LOSS_AVAILABLE:
+ output = log_probs_from_logits_flash_attn(logits, labels)
+ else: # fall back to torch kernel, upcast logits to fp32
+ output = F.cross_entropy(logits.float(), labels, reduction="none")
+
+ return output.view(*batch_dim)
+
+
+def masked_mean(values: torch.Tensor, mask: torch.Tensor, dim: int = None, eps: float = 1e-8) -> torch.Tensor:
+ """Compute mean of tensor with a masked values."""
+ return (values * mask).sum(dim=dim) / (mask.sum(dim=dim) + eps)
+
+
+def masked_var(values: torch.Tensor, mask: torch.Tensor, unbiased: bool = True) -> torch.Tensor:
+ """Compute variance of tensor with masked values."""
+ mean = masked_mean(values, mask)
+ centered_values = values - mean
+ variance = masked_mean(centered_values**2, mask)
+ if unbiased:
+ mask_sum = mask.sum()
+ if mask_sum <= 1:
+ print("The sum of the mask is less than one, which can cause a division by zero.")
+ return variance
+
+ bessel_correction = mask_sum / (mask_sum - 1)
+ variance = variance * bessel_correction
+
+ return variance
+
+
+def masked_whiten(values: torch.Tensor, mask: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
+ """Whiten values with masked values."""
+ mean, var = masked_mean(values, mask), masked_var(values, mask)
+ return (values - mean) * torch.rsqrt(var + eps)
+
+
+def get_response_mask(
+ response_ids: torch.Tensor, eos_token_id: Union[int, List[int]] = 2, dtype: torch.dtype = torch.long
+):
+ """Get the mask for the response ids, the mask will be 0 after the first eos token.
+
+ eos_token_id can be int or list: 1 or [1, 2].
+ ```
+ e.g. eos_token = 1
+ response_ids: [0, 0, 2, 4, 3, 5, 1, 0, 0]
+ response_mask: [1, 1, 1, 1, 1, 1, 1, 0, 0]
+ ```
+ """
+ if isinstance(eos_token_id, int):
+ eos_token_id = [eos_token_id]
+
+ response_mask = torch.zeros_like(response_ids, dtype=torch.bool)
+ for token_id in eos_token_id:
+ response_mask |= response_ids.eq(token_id)
+
+ response_mask = response_mask.long()
+ response_mask = (torch.cumsum(response_mask, dim=1) - response_mask).bool()
+ response_mask = torch.logical_not(response_mask).to(dtype)
+ return response_mask
+
+
+def pad_2d_list_to_length(
+ response: List[List[int]], pad_token_id: int, max_length: Optional[int] = None
+) -> torch.Tensor:
+ """Pad a 2D list (e.g. responses, log_probs) to a 2D tensor."""
+ max_response_length = max(len(sub_list) for sub_list in response)
+ if max_length is not None and max_length > max_response_length:
+ target_length = max_length
+ else:
+ target_length = max_response_length
+
+ padded_response = [tuple(sub_list) + (pad_token_id,) * (target_length - len(sub_list)) for sub_list in response]
+ tensor = torch.tensor(padded_response)
+ return tensor
+
+
+def pad_sequence_to_length(
+ tensor: torch.Tensor, max_seq_len: int, pad_token_id: int, left_pad: bool = False
+) -> torch.Tensor:
+ """Pad a nD tensors in the last dim to max_seq_len."""
+ if tensor.size(-1) >= max_seq_len:
+ return tensor
+
+ pad_shape = list(tensor.shape)
+ pad_shape[-1] = max_seq_len - tensor.size(-1)
+ pad_tensor = torch.full(pad_shape, fill_value=pad_token_id, dtype=tensor.dtype, device=tensor.device)
+ return torch.cat((pad_tensor, tensor), dim=-1) if left_pad else torch.cat((tensor, pad_tensor), dim=-1)
+
+
+def postprocess_data(
+ input_ids: torch.Tensor,
+ attention_mask: torch.Tensor,
+ position_ids: torch.Tensor,
+ max_length: int,
+ pad_token_id: int,
+ left_pad: bool = True,
+ truncation: Literal["left", "right", "error"] = "error",
+):
+ """Pad or truncate data."""
+ assert truncation in ["left", "right", "error"]
+ seq_length = len(input_ids)
+ if seq_length < max_length:
+ input_ids = pad_sequence_to_length(
+ input_ids, max_seq_len=max_length, pad_token_id=pad_token_id, left_pad=left_pad
+ )
+ attention_mask = pad_sequence_to_length(
+ attention_mask, max_seq_len=max_length, pad_token_id=0, left_pad=left_pad
+ )
+ position_ids = pad_sequence_to_length(position_ids, max_seq_len=max_length, pad_token_id=0, left_pad=left_pad)
+ elif seq_length > max_length:
+ if truncation == "left": # actually, left truncation may not be reasonable
+ input_ids = input_ids[..., -max_length:]
+ attention_mask = attention_mask[..., -max_length:]
+ position_ids = position_ids[..., -max_length:]
+ elif truncation == "right":
+ input_ids = input_ids[..., :max_length]
+ attention_mask = attention_mask[..., :max_length]
+ position_ids = position_ids[..., :max_length]
+ elif truncation == "error":
+ raise RuntimeError(f"Input sequence length {seq_length} is longer than max length {max_length}.")
+ else:
+ raise NotImplementedError(f"Unknown truncation method {truncation}.")
+
+ return input_ids, attention_mask, position_ids
+
+
+def get_constant_schedule_with_warmup(
+ optimizer: torch.optim.Optimizer,
+ num_warmup_steps: int,
+ last_epoch: int = -1,
+) -> torch.optim.lr_scheduler.LRScheduler:
+ """Get the lr scheduler for constant lr."""
+
+ def lr_lambda(current_step: int) -> float:
+ return min(1.0, float(current_step) / float(max(1, num_warmup_steps)))
+
+ return LambdaLR(optimizer, lr_lambda, last_epoch)
+
+
+# https://github.com/meta-llama/llama-cookbook/blob/v0.0.5/src/llama_cookbook/policies/anyprecision_optimizer.py
+class AnyPrecisionAdamW(torch.optim.Optimizer):
+ def __init__(
+ self,
+ params: List[torch.Tensor],
+ lr: float = 1e-3,
+ betas: Tuple[float, float] = (0.9, 0.999),
+ eps: float = 1e-8,
+ weight_decay: float = 0.0,
+ use_kahan_summation: bool = True,
+ momentum_dtype: str = "bfloat16",
+ variance_dtype: str = "bfloat16",
+ compensation_buffer_dtype: str = "bfloat16",
+ ):
+ """
+ AnyPrecisionAdamW: a flexible precision AdamW optimizer
+ with optional Kahan summation for high precision weight updates.
+ Allows direct control over momentum, variance and auxiliary compensation buffer dtypes.
+ Optional Kahan summation is used to offset precision reduction for the weight updates.
+ This allows full training in BFloat16 (equal or better than FP32 results in many cases)
+ due to high precision weight updates.
+
+ Args:
+ params (iterable): iterable of parameters to optimize or dicts defining parameter groups
+ lr (float, optional): learning rate (default: 1e-3)
+ betas (Tuple[float, float], optional): coefficients used for computing
+ running averages of gradient and its square (default: (0.9, 0.999))
+ eps (float, optional): term added to the denominator to improve numerical stability (default: 1e-8)
+ weight_decay (float, optional): weight decay coefficient (default: 1e-2)
+
+ # Any Precision specific
+ use_kahan_summation = creates auxiliary buffer to ensure high precision
+ model param updates (default: False)
+ momentum_dtype = dtype for momentum (default: bfloat16)
+ variance_dtype = dtype for uncentered variance (default: bfloat16)
+ compensation_buffer_dtype = dtype for Kahan summation buffer (default: bfloat16)
+
+ # Usage
+ This optimizer implements optimizer states, and Kahan summation
+ for high precision updates, all in user controlled dtypes.
+ Defaults are variance in BF16, Momentum in FP32.
+ This can be run in FSDP mixed precision, amp, or full precision,
+ depending on what training pipeline you wish to work with.
+
+ Setting to use_kahan_summation = False, and changing momentum and
+ variance dtypes to FP32, reverts this to a standard AdamW optimizer.
+
+ """
+ defaults = {
+ "lr": lr,
+ "betas": betas,
+ "eps": eps,
+ "weight_decay": weight_decay,
+ "use_kahan_summation": use_kahan_summation,
+ "momentum_dtype": momentum_dtype,
+ "variance_dtype": variance_dtype,
+ "compensation_buffer_dtype": compensation_buffer_dtype,
+ }
+ super().__init__(params, defaults)
+
+ @torch.no_grad()
+ def step(self, closure=None):
+ """
+ Performs a single optimization step.
+
+ Args:
+ closure (callable, optional): A closure that reevaluates the model and returns the loss.
+ """
+
+ if closure is not None:
+ with torch.enable_grad():
+ closure()
+
+ for group in self.param_groups:
+ beta1, beta2 = group["betas"]
+ lr = group["lr"]
+ weight_decay = group["weight_decay"]
+ eps = group["eps"]
+ use_kahan_summation = group["use_kahan_summation"]
+
+ momentum_dtype = PrecisionType.to_dtype(group["momentum_dtype"])
+ variance_dtype = PrecisionType.to_dtype(group["variance_dtype"])
+ compensation_buffer_dtype = PrecisionType.to_dtype(group["compensation_buffer_dtype"])
+ for p in group["params"]:
+ assert isinstance(p, torch.Tensor) # lint
+ if p.grad is None:
+ continue
+
+ if p.grad.is_sparse:
+ raise RuntimeError("AnyPrecisionAdamW does not support sparse gradients.")
+
+ state = self.state[p]
+ # State initialization
+ if len(state) == 0:
+ state["step"] = torch.tensor(0.0)
+
+ # momentum - EMA of gradient values
+ state["exp_avg"] = torch.zeros_like(p, dtype=momentum_dtype)
+
+ # variance uncentered - EMA of squared gradient values
+ state["exp_avg_sq"] = torch.zeros_like(p, dtype=variance_dtype)
+
+ # optional Kahan summation - accumulated error tracker
+ if use_kahan_summation:
+ state["compensation"] = torch.zeros_like(p, dtype=compensation_buffer_dtype)
+
+ # Main processing
+ # update the steps for each param group update
+ state["step"] += 1
+ step = state["step"]
+
+ exp_avg = state["exp_avg"]
+ exp_avg_sq = state["exp_avg_sq"]
+ grad = p.grad
+
+ if weight_decay: # weight decay, AdamW style
+ p.data.mul_(1 - lr * weight_decay)
+
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) # update momentum
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) # update uncentered variance
+
+ bias_correction1 = 1 - beta1**step # adjust using bias1
+ step_size = lr / bias_correction1
+
+ denom_correction = (1 - beta2**step) ** 0.5 # adjust using bias2 and avoids math import
+ centered_variance = (exp_avg_sq.sqrt() / denom_correction).add_(eps, alpha=1)
+
+ if use_kahan_summation: # lr update to compensation
+ compensation = state["compensation"]
+ compensation.addcdiv_(exp_avg, centered_variance, value=-step_size)
+
+ # update weights with compensation (Kahan summation)
+ # save error back to compensation for next iteration
+ temp_buffer = p.detach().clone()
+ p.data.add_(compensation)
+ compensation.add_(temp_buffer.sub_(p.data))
+ else: # usual AdamW updates
+ p.data.addcdiv_(exp_avg, centered_variance, value=-step_size)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/ulysses.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/ulysses.py
new file mode 100644
index 0000000000000000000000000000000000000000..18e07b4ab4be37cb12bda078df94d8cce83416d6
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/ulysses.py
@@ -0,0 +1,298 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Utilities for DeepSpeed Ulysses Sequence Parallelism.
+DeepSpeed Ulysses Paper: https://arxiv.org/abs/2309.14509
+Inspired from: https://github.com/microsoft/DeepSpeed/blob/master/deepspeed/sequence/layer.py
+"""
+
+from typing import Any, Optional, Tuple
+
+import torch
+import torch.distributed as dist
+from torch import Tensor
+from torch.distributed import ProcessGroup
+
+
+_ULYSSES_SEQUENCE_PARALLEL_GROUP = None
+
+
+def set_ulysses_sequence_parallel_group(group: dist.ProcessGroup):
+ """
+ Set ulysses sequence parallel process group.
+ """
+ global _ULYSSES_SEQUENCE_PARALLEL_GROUP
+ _ULYSSES_SEQUENCE_PARALLEL_GROUP = group
+
+
+def get_ulysses_sequence_parallel_group() -> Optional[dist.ProcessGroup]:
+ """
+ Get ulysses sequence parallel process group.
+ """
+ global _ULYSSES_SEQUENCE_PARALLEL_GROUP
+ return _ULYSSES_SEQUENCE_PARALLEL_GROUP
+
+
+def get_ulysses_sequence_parallel_world_size(group: ProcessGroup = None) -> int:
+ """
+ Get ulysses sequence parallel world size.
+ """
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ return dist.get_world_size(group) if group else 1
+
+
+def get_ulysses_sequence_parallel_rank(group: ProcessGroup = None) -> int:
+ """
+ Get ulysses sequence parallel rank.
+ """
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ return dist.get_rank(group) if group else 0
+
+
+def gather_seq_scatter_heads(
+ x: Tensor,
+ seq_dim: int,
+ head_dim: int,
+ unpadded_dim_size: int = 0,
+ group: ProcessGroup = None,
+) -> Tensor:
+ """
+ A func to sync embedding input with alltoall in sequence parallel
+ gather sequence dimension and scatter head dim:
+ e.g. seq_dim: 1, head_dim: 2
+ [bsz, seq/n, h, ...] -> [bsz, seq, h/n, ...]
+ """
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ if not group:
+ return x
+ sp_world = get_ulysses_sequence_parallel_world_size(group)
+ x = SeqAllToAll.apply(group, x, head_dim, seq_dim)
+ if unpadded_dim_size and unpadded_dim_size % sp_world != 0:
+ padding_size = x.size(seq_dim) - unpadded_dim_size
+ x = _unpad_tensor(x, seq_dim, padding_size)
+ return x
+
+
+def gather_heads_scatter_seq(x: Tensor, head_dim: int, seq_dim: int, group: ProcessGroup = None) -> Tensor:
+ """
+ A func to sync attention result with alltoall in sequence parallel
+ gather head dimension and scatter seq dim:
+ e.g. seq_dim: 1, head_dim: 2
+ [bsz, seq, h/n, ...] -> [bsz, seq/n, h, ...]
+ """
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ if not group:
+ return x
+ dim_size = x.size(seq_dim)
+ sp_world = get_ulysses_sequence_parallel_world_size(group)
+ if dim_size % sp_world != 0:
+ padding_size = sp_world - (dim_size % sp_world)
+ x = _pad_tensor(x, seq_dim, padding_size)
+ return SeqAllToAll.apply(group, x, seq_dim, head_dim, False)
+
+
+def _pad_tensor(x: Tensor, dim: int, padding_size: int) -> Tensor:
+ shape = list(x.shape)
+ shape[dim] = padding_size
+ pad = torch.zeros(shape, dtype=x.dtype, device=x.device)
+ return torch.cat([x, pad], dim=dim)
+
+
+def _unpad_tensor(x: Tensor, dim: int, padding_size: int) -> Tensor:
+ slc = [slice(None)] * len(x.shape)
+ slc[dim] = slice(0, -padding_size)
+ return x[slc]
+
+
+def slice_input_tensor(x: Tensor, dim: int, padding: bool = True, group: ProcessGroup = None) -> Tensor:
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ sp_world_size = dist.get_world_size(group)
+ sp_rank = get_ulysses_sequence_parallel_rank()
+ dim_size = x.size(dim)
+ # pad before slice
+ if padding and dim_size % sp_world_size:
+ padding_size = sp_world_size - (dim_size % sp_world_size)
+ x = _pad_tensor(x, dim, padding_size)
+ # slice the input tensor
+ parts = x.size(dim) // sp_world_size
+ slc = [slice(None)] * len(x.shape)
+ slc[dim] = slice(sp_rank * parts, (sp_rank + 1) * parts)
+ return x[slc].contiguous()
+
+
+def all_to_all_tensor(
+ local_input: Tensor,
+ scatter_dim: int,
+ gather_dim: int,
+ group: Optional[dist.ProcessGroup] = None,
+ async_op: bool = False,
+):
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ seq_world_size = dist.get_world_size(group)
+ input_list = [t.contiguous() for t in torch.tensor_split(local_input, seq_world_size, scatter_dim)]
+ output_list = [torch.empty_like(input_list[0]) for _ in range(seq_world_size)]
+ comm = dist.all_to_all(output_list, input_list, group=group, async_op=async_op)
+ if async_op:
+
+ def wait():
+ comm.wait()
+ return torch.cat(output_list, dim=gather_dim).contiguous()
+
+ return wait
+ return torch.cat(output_list, dim=gather_dim).contiguous()
+
+
+def all_gather_tensor(local_tensor: Tensor, group: Optional[dist.ProcessGroup] = None, async_op: bool = False):
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ sp_world_size = dist.get_world_size(group=group)
+ output_shape = list(local_tensor.shape)
+ output_shape[0] = output_shape[0] * sp_world_size
+ output = torch.empty(output_shape, dtype=local_tensor.dtype, device=local_tensor.device)
+ dist.all_gather_into_tensor(output, local_tensor, group=group, async_op=async_op)
+ return output
+
+
+class SeqAllToAll(torch.autograd.Function):
+ @staticmethod
+ def forward(
+ ctx: Any,
+ group: dist.ProcessGroup,
+ local_input: Tensor,
+ scatter_dim: int,
+ gather_dim: int,
+ async_op: bool = False,
+ ) -> Tensor:
+ ctx.group = group
+ ctx.scatter_dim = scatter_dim
+ ctx.gather_dim = gather_dim
+ ctx.async_op = async_op
+ return all_to_all_tensor(local_input, scatter_dim, gather_dim, group, async_op)
+
+ @staticmethod
+ def backward(ctx: Any, *grad_output: Tensor) -> Tuple[None, Tensor, None, None]:
+ if ctx.async_op:
+ input_t = torch.cat(grad_output[1:], dim=ctx.gather_dim).contiguous()
+ else:
+ input_t = grad_output[0]
+ return (
+ None,
+ all_to_all_tensor(input_t, ctx.gather_dim, ctx.scatter_dim, ctx.group, False),
+ None,
+ None,
+ None,
+ None,
+ )
+
+
+class Gather(torch.autograd.Function):
+ @staticmethod
+ def forward(
+ ctx: Any,
+ group: dist.ProcessGroup,
+ local_tensor: Tensor,
+ gather_dim: int,
+ grad_scaler: bool = True,
+ async_op=False,
+ ) -> Tensor:
+ ctx.group = group
+ ctx.gather_dim = gather_dim
+ ctx.grad_scaler = grad_scaler
+ ctx.async_op = async_op
+
+ sp_world_size = dist.get_world_size(group=group)
+ ctx.sp_world_size = sp_world_size
+
+ sp_rank = dist.get_rank(group=group)
+ ctx.sp_rank = sp_rank
+
+ local_shape = list(local_tensor.size())
+ split_size = local_shape[0]
+ part_size = local_shape[gather_dim] # store original size
+ ctx.part_size = part_size
+
+ output = all_gather_tensor(local_tensor, group, async_op)
+ return torch.cat(output.split(split_size, dim=0), dim=gather_dim)
+
+ @staticmethod
+ def backward(ctx: Any, grad_output: Tensor) -> Any:
+ if ctx.grad_scaler:
+ grad_output = grad_output * ctx.sp_world_size
+ return (
+ None,
+ grad_output.split(ctx.part_size, dim=ctx.gather_dim)[ctx.sp_rank].contiguous(),
+ None,
+ None,
+ None,
+ None,
+ )
+
+
+def gather_outputs_and_unpad(
+ x: Tensor,
+ gather_dim: int,
+ unpad_dim: int = None,
+ padding_size: int = 0,
+ grad_scaler: bool = True,
+ group: Optional[dist.ProcessGroup] = None,
+):
+ group = get_ulysses_sequence_parallel_group() if group is None else group
+ # sp_size = get_ulysses_sequence_parallel_world_size()
+ if group is None:
+ return x
+ x = Gather.apply(group, x, gather_dim, grad_scaler)
+ if unpad_dim is not None:
+ assert isinstance(padding_size, int), "padding size is not given or is not an integer"
+ if padding_size == 0:
+ return x
+ x = _unpad_tensor(x, unpad_dim, padding_size)
+ return x
+
+
+def ulysses_pad_and_slice_inputs(
+ input_ids_rmpad: torch.Tensor, position_ids_rmpad: Optional[torch.Tensor] = None, sp_size: int = 1
+):
+ """
+ Pad and slice input_ids to be divisible by sp_size
+ Pad position_ids to be divisible by sp_size.
+
+ Note both input_ids_rmpad and position_ids_rmpad will be padded,
+ but only input_ids will be sliced.
+
+ The is the utility of pre-forward for ulysses sequence parallelism
+
+ Args:
+ input_ids_rmpad: shape of [bsz, seqlen]
+ position_ids_rmpad: shape of [bsz, seqlen], where bsz must be 1
+ sp_size (int): ulysses sequence parallelism size
+
+ Returns:
+ torch.Tensor: padded and sliced input_ids
+ torch.Tensor: padded and sliced position_ids
+ int: pad size
+ """
+ if position_ids_rmpad is not None:
+ assert position_ids_rmpad.size(0) == 1
+ assert input_ids_rmpad.size(1) == position_ids_rmpad.size(1)
+ if sp_size <= 1:
+ return input_ids_rmpad, position_ids_rmpad, 0
+ _, total_seq_len = input_ids_rmpad.shape
+ pad_size = (sp_size - total_seq_len % sp_size) % sp_size
+ if pad_size > 0:
+ input_ids_rmpad = torch.nn.functional.pad(input_ids_rmpad, (0, pad_size), value=0)
+ if position_ids_rmpad is not None:
+ pad_pos_ids = torch.arange(pad_size, device=position_ids_rmpad.device).unsqueeze(0)
+ position_ids_rmpad = torch.cat((position_ids_rmpad, pad_pos_ids), dim=-1)
+ # we don't need to slice position ids
+ input_ids_rmpad = slice_input_tensor(input_ids_rmpad, dim=1, padding=False)
+ return input_ids_rmpad, position_ids_rmpad, pad_size
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ce90c5eb352d85c59105c0dc85b5f1dd576f095
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7472ab3b6d5ec7f656c39ea6613bd066b8048b10
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/__init__.py
@@ -0,0 +1,24 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .config import ActorConfig, FSDPConfig, ModelConfig, OptimConfig, RefConfig
+
+
+__all__ = [
+ "ActorConfig",
+ "FSDPConfig",
+ "ModelConfig",
+ "OptimConfig",
+ "RefConfig",
+]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/base.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd264caf68e7369660006200105181aa5b4eb187
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/base.py
@@ -0,0 +1,64 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+The base class for Actor
+"""
+
+from abc import ABC, abstractmethod
+from typing import Any, Dict
+
+import torch
+
+from ...protocol import DataProto
+from .config import ActorConfig
+
+
+__all__ = ["BasePPOActor"]
+
+
+class BasePPOActor(ABC):
+ def __init__(self, config: ActorConfig):
+ """The base class for PPO actor
+
+ Args:
+ config (ActorConfig): a config passed to the PPOActor.
+ """
+ self.config = config
+
+ @abstractmethod
+ def compute_log_prob(self, data: DataProto) -> torch.Tensor:
+ """Compute logits given a batch of data.
+
+ Args:
+ data (DataProto): a batch of data represented by DataProto. It must contain key ```input_ids```,
+ ```attention_mask``` and ```position_ids```.
+
+ Returns:
+ DataProto: a DataProto containing the key ```log_probs```
+ """
+ pass
+
+ @abstractmethod
+ def update_policy(self, data: DataProto) -> Dict[str, Any]:
+ """Update the policy with an iterator of DataProto
+
+ Args:
+ data (DataProto): an iterator over the DataProto that returns by
+ ```make_minibatch_iterator```
+
+ Returns:
+ Dict: a dictionary contains anything. Typically, it contains the statistics during updating the model
+ such as ```loss```, ```grad_norm```, etc,.
+ """
+ pass
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/config.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cafef619f369ab449f92c7872c56f591a4de4dd
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/config.py
@@ -0,0 +1,112 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Actor config
+"""
+
+import os
+from dataclasses import dataclass, field
+from typing import Any, Dict, Optional, Tuple
+
+
+@dataclass
+class ModelConfig:
+ model_path: Optional[str] = None
+ tokenizer_path: Optional[str] = None
+ override_config: Dict[str, Any] = field(default_factory=dict)
+ enable_gradient_checkpointing: bool = True
+ trust_remote_code: bool = True
+ freeze_vision_tower: bool = False
+
+ def post_init(self):
+ if self.tokenizer_path is None:
+ self.tokenizer_path = self.model_path
+
+ if self.model_path is not None and os.path.exists(self.model_path): # ray job uses absolute path
+ self.model_path = os.path.abspath(self.model_path)
+
+ if self.tokenizer_path is not None and os.path.exists(self.tokenizer_path):
+ self.tokenizer_path = os.path.abspath(self.tokenizer_path)
+
+
+@dataclass
+class OptimConfig:
+ lr: float = 1e-6
+ betas: Tuple[float, float] = (0.9, 0.999)
+ weight_decay: float = 1e-2
+ strategy: str = "adamw"
+ lr_warmup_ratio: float = 0.0
+ min_lr_ratio: Optional[float] = None
+ warmup_style: str = "constant"
+ """auto keys"""
+ training_steps: int = field(default=-1, init=False)
+
+
+@dataclass
+class FSDPConfig:
+ enable_full_shard: bool = True
+ enable_cpu_offload: bool = False
+ enable_rank0_init: bool = False
+ use_orig_params: bool = False
+ torch_dtype: Optional[str] = None
+ fsdp_size: int = -1
+ mp_param_dtype: str = "bf16"
+ mp_reduce_dtype: str = "fp32"
+ mp_buffer_dtype: str = "fp32"
+
+
+@dataclass
+class OffloadConfig:
+ offload_params: bool = False
+ offload_optimizer: bool = False
+
+
+@dataclass
+class ActorConfig:
+ strategy: str = "fsdp"
+ global_batch_size: int = 256
+ micro_batch_size_per_device_for_update: int = 4
+ micro_batch_size_per_device_for_experience: int = 16
+ max_grad_norm: float = 1.0
+ clip_ratio_low: float = 0.2
+ clip_ratio_high: float = 0.3
+ clip_ratio_dual: float = 3.0
+ ppo_epochs: int = 1
+ padding_free: bool = False
+ ulysses_sequence_parallel_size: int = 1
+ use_torch_compile: bool = True
+ model: ModelConfig = field(default_factory=ModelConfig)
+ optim: OptimConfig = field(default_factory=OptimConfig)
+ fsdp: FSDPConfig = field(default_factory=FSDPConfig)
+ offload: OffloadConfig = field(default_factory=OffloadConfig)
+ """auto keys"""
+ global_batch_size_per_device: int = field(default=-1, init=False)
+ disable_kl: bool = field(default=False, init=False)
+ use_kl_loss: bool = field(default=False, init=False)
+ kl_penalty: str = field(default="kl", init=False)
+ kl_coef: float = field(default=0.0, init=False)
+ entropy_coef: float = field(default=0.0, init=False)
+ is_entropy_reg: bool = True
+
+
+@dataclass
+class RefConfig:
+ strategy: str = "fsdp"
+ fsdp: FSDPConfig = field(default_factory=FSDPConfig)
+ offload: OffloadConfig = field(default_factory=OffloadConfig)
+ """auto keys"""
+ micro_batch_size_per_device_for_experience: int = field(default=-1, init=False)
+ padding_free: bool = field(default=False, init=False)
+ ulysses_sequence_parallel_size: int = field(default=1, init=False)
+ use_torch_compile: bool = field(default=True, init=False)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/dp_actor.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/dp_actor.py
new file mode 100644
index 0000000000000000000000000000000000000000..37d5b92003045eff9c68c0c4429ea64330894908
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/actor/dp_actor.py
@@ -0,0 +1,344 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Implement Actor (dtype-safe SDPA mask handling)
+"""
+
+import os
+from collections import defaultdict
+from typing import Any, Dict, Optional
+
+import torch
+import numpy as np
+from einops import rearrange
+from ray.experimental.tqdm_ray import tqdm
+from torch import nn
+from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+from transformers.modeling_flash_attention_utils import (
+ index_first_axis,
+ pad_input,
+ unpad_input,
+)
+
+from ...protocol import DataProto
+from ...trainer import core_algos
+from ...utils import torch_functional as VF
+from ...utils.py_functional import append_to_dict
+from ...utils.ulysses import gather_outputs_and_unpad, ulysses_pad_and_slice_inputs
+from .base import BasePPOActor
+from .config import ActorConfig
+
+
+__all__ = ["DataParallelPPOActor"]
+
+
+class DataParallelPPOActor(BasePPOActor):
+ def __init__(
+ self,
+ config: ActorConfig,
+ actor_module: nn.Module,
+ actor_optimizer: Optional[torch.optim.Optimizer] = None,
+ ):
+ """
+ When optimizer is None, it is Reference Policy
+ """
+ super().__init__(config)
+ self.rank = int(os.getenv("RANK", "0"))
+ self.actor_module = actor_module
+ self.actor_optimizer = actor_optimizer
+ if config.use_torch_compile:
+ self.log_probs_from_logits = torch.compile(VF.log_probs_from_logits, dynamic=True)
+ else:
+ self.log_probs_from_logits = VF.log_probs_from_logits
+
+ def _forward_micro_batch(self, micro_batch: Dict[str, torch.Tensor], temperature: float) -> torch.Tensor:
+ """
+ Returns:
+ log_probs: # (bs, response_len)
+ """
+ input_ids = micro_batch["input_ids"]
+ batch_size, seqlen = input_ids.shape
+ attention_mask = micro_batch["attention_mask"]
+ position_ids = micro_batch["position_ids"]
+ responses = micro_batch["responses"]
+ response_length = responses.size(-1)
+
+ # mrope for qwen2vl: (bsz, 3, seqlen) -> (3, bsz, seqlen)
+ if position_ids.dim() == 3:
+ position_ids = position_ids.transpose(0, 1)
+
+ if attention_mask.dtype != torch.bool:
+ attention_mask = attention_mask.ne(0)
+
+ multi_modal_inputs: Dict[str, torch.Tensor] = {}
+ if "multi_modal_inputs" in micro_batch:
+ mm_list = micro_batch["multi_modal_inputs"] # list[dict]
+ for i in range(len(mm_list)):
+ for key in ("pixel_values", "aspect_ratio_ids", "aspect_ratio_mask", "num_tiles"):
+ if key not in mm_list[i]:
+ continue
+ x = mm_list[i][key]
+ if not torch.is_tensor(x):
+ try:
+ x = torch.from_numpy(x) if isinstance(x, np.ndarray) else torch.tensor(x)
+ except Exception:
+ x = torch.tensor(x)
+ if key == "pixel_values":
+ if (not x.dtype.is_floating_point) or (x.dtype != torch.float32):
+ x = x.to(torch.float32)
+ elif key == "aspect_ratio_mask":
+ if x.dtype != torch.bool:
+ x = x.ne(0)
+ else:
+ if x.dtype != torch.long:
+ x = x.to(torch.long)
+
+ mm_list[i][key] = x
+
+ for key in mm_list[0].keys():
+ tensors = [d[key] for d in mm_list if key in d]
+ multi_modal_inputs[key] = torch.cat(tensors, dim=0)
+
+ if self.config.padding_free:
+ input_ids_rmpad, indices, *_ = unpad_input(
+ input_ids.unsqueeze(-1), attention_mask
+ ) # input_ids_rmpad (total_nnz, ...)
+ input_ids_rmpad = input_ids_rmpad.transpose(0, 1) # (1, total_nnz)
+
+ # unpad the position_ids to align the rotary
+ if position_ids.dim() == 3:
+ position_ids_rmpad = (
+ index_first_axis(rearrange(position_ids, "c b s ... -> (b s) c ..."), indices)
+ .transpose(0, 1)
+ .unsqueeze(1)
+ ) # (3, bsz, seqlen) -> (3, 1, bsz * seqlen)
+ else:
+ position_ids_rmpad = index_first_axis(
+ rearrange(position_ids.unsqueeze(-1), "b s ... -> (b s) ..."), indices
+ ).transpose(0, 1)
+
+ # for compute the log_prob
+ input_ids_rmpad_rolled = torch.roll(input_ids_rmpad, shifts=-1, dims=1) # (1, total_nnz)
+
+ # pad and slice the inputs if sp > 1
+ if self.config.ulysses_sequence_parallel_size > 1:
+ input_ids_rmpad, position_ids_rmpad, pad_size = ulysses_pad_and_slice_inputs(
+ input_ids_rmpad, position_ids_rmpad, sp_size=self.config.ulysses_sequence_parallel_size
+ )
+ input_ids_rmpad_rolled, _, _ = ulysses_pad_and_slice_inputs(
+ input_ids_rmpad_rolled, None, self.config.ulysses_sequence_parallel_size
+ )
+
+ input_ids_rmpad_rolled = input_ids_rmpad_rolled.squeeze(0) # ((total_nnz / sp) + pad)
+
+ # only pass input_ids and position_ids to enable flash_attn_varlen
+ output = self.actor_module(
+ input_ids=input_ids_rmpad,
+ attention_mask=None,
+ position_ids=position_ids_rmpad,
+ **multi_modal_inputs,
+ use_cache=False,
+ ) # prevent model thinks we are generating
+ logits_rmpad = output.logits.squeeze(0) # (total_nnz, vocab_size)
+ logits_rmpad.div_(temperature)
+ # ((total_nnz / sp) + pad)
+ log_probs = self.log_probs_from_logits(logits=logits_rmpad, labels=input_ids_rmpad_rolled)
+
+ # gather log_prob if sp > 1
+ if self.config.ulysses_sequence_parallel_size > 1:
+ # gather and unpad for the ulysses sp
+ log_probs = gather_outputs_and_unpad(log_probs, gather_dim=0, unpad_dim=0, padding_size=pad_size)
+
+ # pad back to (bsz, seqlen)
+ full_log_probs = pad_input(
+ hidden_states=log_probs.unsqueeze(-1), indices=indices, batch=batch_size, seqlen=seqlen
+ )
+ log_probs = full_log_probs.squeeze(-1)[:, -response_length - 1 : -1] # (bsz, response_length)
+
+ full_logits = pad_input(
+ hidden_states=logits_rmpad.unsqueeze(-1), indices=indices, batch=batch_size, seqlen=seqlen
+ )
+ logits = full_logits.squeeze(-1)[:, -response_length - 1 : -1, :] # (bsz, response_length, vocab_size)
+ else:
+ output = self.actor_module(
+ input_ids=input_ids,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ **multi_modal_inputs,
+ use_cache=False,
+ )
+ logits: torch.Tensor = output.logits
+ logits.div_(temperature)
+ logits = logits[:, -response_length - 1 : -1, :] # (bsz, response_length, vocab_size)
+ log_probs = self.log_probs_from_logits(logits, responses) # (bsz, response_length)
+
+ return log_probs, logits
+
+ def _optimizer_step(self) -> torch.Tensor:
+ if isinstance(self.actor_module, FSDP):
+ grad_norm = self.actor_module.clip_grad_norm_(self.config.max_grad_norm)
+ else:
+ grad_norm = nn.utils.clip_grad_norm_(self.actor_module.parameters(), max_norm=self.config.max_grad_norm)
+
+ if not torch.isfinite(grad_norm):
+ print("Gradient norm is not finite. Skip update.")
+ else:
+ self.actor_optimizer.step()
+
+ self.actor_optimizer.zero_grad()
+ return grad_norm
+
+ @torch.no_grad()
+ def compute_log_prob(self, data: DataProto) -> torch.Tensor:
+ """Compute the log probability of the responses given input_ids, attention_mask and position_ids
+
+ Args:
+ data (DataProto): a DataProto containing keys
+
+ ``input_ids``: tensor of shape [batch_size, sequence_length]. torch.int64. Note that input_ids is the
+ concatenation of prompt and response. Note that ``sequence_length = prompt_length + response_length``.
+
+ ``attention_mask``: tensor of shape [batch_size, sequence_length]. torch.int64.
+
+ ``position_ids``: tensor of shape [batch_size, sequence_length]. torch.int64.
+
+ ``responses``: tensor of shape [batch_size, response_length]. torch.int64.
+
+ Returns:
+ torch.Tensor: the log_prob tensor
+ """
+ self.actor_module.eval()
+
+ temperature = data.meta_info["temperature"]
+ select_keys = ["responses", "input_ids", "attention_mask", "position_ids"]
+ if "multi_modal_inputs" in data.non_tensor_batch.keys():
+ non_tensor_select_keys = ["multi_modal_inputs"]
+ else:
+ non_tensor_select_keys = []
+
+ micro_batches = data.select(select_keys, non_tensor_select_keys).split(
+ self.config.micro_batch_size_per_device_for_experience
+ )
+ log_probs_lst = []
+ if self.rank == 0:
+ micro_batches = tqdm(micro_batches, desc="Compute log probs", position=2)
+
+ for micro_batch in micro_batches:
+ model_inputs = {**micro_batch.batch, **micro_batch.non_tensor_batch}
+ log_probs, _ = self._forward_micro_batch(model_inputs, temperature=temperature)
+ log_probs_lst.append(log_probs)
+
+ log_probs = torch.concat(log_probs_lst, dim=0)
+ return log_probs
+
+ def update_policy(self, data: DataProto) -> Dict[str, Any]:
+ self.actor_module.train()
+
+ temperature = data.meta_info["temperature"] # temperature must be in the data.meta_info to avoid slient error
+ select_keys = [
+ "responses",
+ "input_ids",
+ "attention_mask",
+ "position_ids",
+ "old_log_probs",
+ "advantages",
+ ]
+ if self.config.use_kl_loss and not self.config.disable_kl:
+ select_keys.append("ref_log_probs")
+
+ if "multi_modal_inputs" in data.non_tensor_batch.keys():
+ non_tensor_select_keys = ["multi_modal_inputs"]
+ else:
+ non_tensor_select_keys = []
+
+ # Split to make minibatch iterator for updating the actor
+ # See PPO paper for details. https://arxiv.org/abs/1707.06347
+ mini_batches = data.select(select_keys, non_tensor_select_keys).split(self.config.global_batch_size_per_device)
+
+ metrics = defaultdict(list)
+ for _ in range(self.config.ppo_epochs):
+ if self.rank == 0:
+ mini_batches = tqdm(mini_batches, desc="Train mini-batches", position=2)
+
+ for mini_batch in mini_batches:
+ gradient_accumulation = (
+ self.config.global_batch_size_per_device // self.config.micro_batch_size_per_device_for_update
+ )
+ micro_batches = mini_batch.split(self.config.micro_batch_size_per_device_for_update)
+ if self.rank == 0:
+ micro_batches = tqdm(micro_batches, desc="Update policy", position=3)
+
+ for micro_batch in micro_batches:
+ model_inputs = {**micro_batch.batch, **micro_batch.non_tensor_batch}
+ responses = model_inputs["responses"]
+ response_length = responses.size(1)
+ attention_mask = model_inputs["attention_mask"]
+ # Keep response_mask boolean as well
+ if attention_mask.dtype != torch.bool:
+ attention_mask = attention_mask.ne(0)
+ response_mask = attention_mask[:, -response_length:]
+ old_log_probs = model_inputs["old_log_probs"]
+ advantages = model_inputs["advantages"]
+
+ # all return: (bsz, response_length)
+ log_probs, logits = self._forward_micro_batch(model_inputs, temperature=temperature)
+ entropy_loss = -VF.masked_mean(log_probs, response_mask) # estimator of entropy loss
+
+ probs = torch.softmax(logits, dim=-1)
+ log_probs_all = torch.log_softmax(logits, dim=-1)
+ entropy = -(probs * log_probs_all).sum(-1)
+ new_entropy_loss = VF.masked_mean(entropy, response_mask)
+
+ pg_loss, pg_clipfrac_higher, pg_clipfrac_lower, ppo_kl = core_algos.compute_policy_loss(
+ old_log_probs=old_log_probs,
+ log_probs=log_probs,
+ advantages=advantages,
+ response_mask=response_mask,
+ clip_ratio_low=self.config.clip_ratio_low,
+ clip_ratio_high=self.config.clip_ratio_high,
+ clip_ratio_dual=self.config.clip_ratio_dual,
+ )
+ if "ref_log_probs" in model_inputs:
+ ref_log_probs = model_inputs["ref_log_probs"]
+ # compute kl loss
+ kld = core_algos.compute_kl(
+ log_probs=log_probs,
+ ref_log_probs=ref_log_probs,
+ kl_penalty=self.config.kl_penalty,
+ )
+ kl_loss = VF.masked_mean(kld, response_mask)
+ pg_loss = pg_loss + kl_loss * self.config.kl_coef
+ metrics["actor/kl_loss"] = kl_loss.detach().item()
+ metrics["actor/kl_coef"] = self.config.kl_coef
+
+ if self.config.is_entropy_reg:
+ loss = (pg_loss + self.config.entropy_coef * new_entropy_loss) / gradient_accumulation
+ else:
+ loss = pg_loss / gradient_accumulation
+ loss.backward()
+
+ batch_metrics = {
+ "actor/pg_loss": pg_loss.detach().item(),
+ "actor/pg_clipfrac_higher": pg_clipfrac_higher.detach().item(),
+ "actor/pg_clipfrac_lower": pg_clipfrac_lower.detach().item(),
+ "actor/entropy_loss": entropy_loss.detach().item(),
+ "actor/new_entropy_loss": new_entropy_loss.detach().item(),
+ "actor/ppo_kl": ppo_kl.detach().item(),
+ }
+ append_to_dict(metrics, batch_metrics)
+
+ grad_norm = self._optimizer_step()
+ append_to_dict(metrics, {"actor/grad_norm": grad_norm.detach().item()})
+
+ return metrics
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/config.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba21b0e79d65788689608b103be53adf45a2c246
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/config.py
@@ -0,0 +1,52 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+ActorRolloutRef config
+"""
+
+from dataclasses import dataclass, field
+
+from .actor import ActorConfig, FSDPConfig, ModelConfig, OptimConfig, RefConfig
+from .critic import CriticConfig
+from .reward import RewardConfig
+from .rollout import RolloutConfig
+
+
+__all__ = [
+ "ActorConfig",
+ "CriticConfig",
+ "FSDPConfig",
+ "ModelConfig",
+ "OptimConfig",
+ "RefConfig",
+ "RewardConfig",
+ "RolloutConfig",
+ "WorkerConfig",
+]
+
+
+@dataclass
+class WorkerConfig:
+ hybrid_engine: bool = True
+ actor: ActorConfig = field(default_factory=ActorConfig)
+ critic: CriticConfig = field(default_factory=CriticConfig)
+ ref: RefConfig = field(default_factory=RefConfig)
+ reward: RewardConfig = field(default_factory=RewardConfig)
+ rollout: RolloutConfig = field(default_factory=RolloutConfig)
+
+ def post_init(self):
+ self.ref.micro_batch_size_per_device_for_experience = self.actor.micro_batch_size_per_device_for_experience
+ self.ref.padding_free = self.actor.padding_free
+ self.ref.ulysses_sequence_parallel_size = self.actor.ulysses_sequence_parallel_size
+ self.ref.use_torch_compile = self.actor.use_torch_compile
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..0bc8ee4caf7a6d9b791b6485320274f85b869069
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .config import CriticConfig
+
+
+__all__ = ["CriticConfig"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/base.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d541467cc450271a396642d541c11221cb322ac
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/base.py
@@ -0,0 +1,42 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Base class for Critic
+"""
+
+from abc import ABC, abstractmethod
+from typing import Any, Dict
+
+import torch
+
+from ...protocol import DataProto
+from .config import CriticConfig
+
+
+__all__ = ["BasePPOCritic"]
+
+
+class BasePPOCritic(ABC):
+ def __init__(self, config: CriticConfig):
+ self.config = config
+
+ @abstractmethod
+ def compute_values(self, data: DataProto) -> torch.Tensor:
+ """Compute values"""
+ pass
+
+ @abstractmethod
+ def update_critic(self, data: DataProto) -> Dict[str, Any]:
+ """Update the critic"""
+ pass
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/config.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..d18d2f0990f7479d76720e69b8a7d25a01e248b5
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/config.py
@@ -0,0 +1,39 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Critic config
+"""
+
+from dataclasses import dataclass, field
+
+from ..actor.config import FSDPConfig, ModelConfig, OffloadConfig, OptimConfig
+
+
+@dataclass
+class CriticConfig:
+ strategy: str = "fsdp"
+ global_batch_size: int = 256
+ micro_batch_size_per_device_for_update: int = 4
+ micro_batch_size_per_device_for_experience: int = 16
+ max_grad_norm: float = 1.0
+ cliprange_value: float = 0.5
+ ppo_epochs: int = 1
+ padding_free: bool = False
+ ulysses_sequence_parallel_size: int = 1
+ model: ModelConfig = field(default_factory=ModelConfig)
+ optim: OptimConfig = field(default_factory=OptimConfig)
+ fsdp: FSDPConfig = field(default_factory=FSDPConfig)
+ offload: OffloadConfig = field(default_factory=OffloadConfig)
+ """auto keys"""
+ global_batch_size_per_device: int = field(default=-1, init=False)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/dp_critic.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/dp_critic.py
new file mode 100644
index 0000000000000000000000000000000000000000..013c8e559701c7715085049e488adb91f7d42531
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/critic/dp_critic.py
@@ -0,0 +1,225 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Implement Critic
+"""
+
+import os
+from collections import defaultdict
+from typing import Any, Dict
+
+import torch
+from ray.experimental.tqdm_ray import tqdm
+from torch import nn
+from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+
+from ...protocol import DataProto
+from ...trainer import core_algos
+from ...utils import torch_functional as VF
+from ...utils.py_functional import append_to_dict
+from ...utils.ulysses import gather_outputs_and_unpad, ulysses_pad_and_slice_inputs
+from .base import BasePPOCritic
+from .config import CriticConfig
+
+
+try:
+ from flash_attn.bert_padding import index_first_axis, pad_input, rearrange, unpad_input
+except ImportError:
+ pass
+
+
+__all__ = ["DataParallelPPOCritic"]
+
+
+class DataParallelPPOCritic(BasePPOCritic):
+ def __init__(self, config: CriticConfig, critic_module: nn.Module, critic_optimizer: torch.optim.Optimizer):
+ super().__init__(config)
+ self.rank = int(os.getenv("RANK", "0"))
+ self.critic_module = critic_module
+ self.critic_optimizer = critic_optimizer
+
+ def _forward_micro_batch(self, micro_batch: Dict[str, torch.Tensor]) -> torch.Tensor:
+ input_ids = micro_batch["input_ids"]
+ batch_size, seqlen = input_ids.shape
+ attention_mask = micro_batch["attention_mask"]
+ position_ids = micro_batch["position_ids"]
+ responses = micro_batch["responses"]
+ response_length = responses.size(-1)
+ if position_ids.dim() == 3: # qwen2vl mrope
+ position_ids = position_ids.transpose(0, 1) # (bsz, 3, seqlen) -> (3, bsz, seqlen)
+
+ multi_modal_inputs = {}
+ if "multi_modal_inputs" in micro_batch:
+ for key in micro_batch["multi_modal_inputs"][0].keys():
+ multi_modal_inputs[key] = torch.cat(
+ [inputs[key] for inputs in micro_batch["multi_modal_inputs"]], dim=0
+ )
+
+ if self.config.padding_free:
+ input_ids_rmpad, indices, *_ = unpad_input(
+ input_ids.unsqueeze(-1), attention_mask
+ ) # input_ids_rmpad (total_nnz, ...)
+ input_ids_rmpad = input_ids_rmpad.transpose(0, 1) # (1, total_nnz)
+
+ # unpad the position_ids to align the rotary
+ if position_ids.dim() == 3:
+ position_ids_rmpad = (
+ index_first_axis(rearrange(position_ids, "c b s ... -> (b s) c ..."), indices)
+ .transpose(0, 1)
+ .unsqueeze(1)
+ ) # (3, bsz, seqlen) -> (3, 1, bsz * seqlen)
+ else:
+ position_ids_rmpad = index_first_axis(
+ rearrange(position_ids.unsqueeze(-1), "b s ... -> (b s) ..."), indices
+ ).transpose(0, 1)
+
+ # pad and slice the inputs if sp > 1
+ if self.config.ulysses_sequence_parallel_size > 1:
+ input_ids_rmpad, position_ids_rmpad, pad_size = ulysses_pad_and_slice_inputs(
+ input_ids_rmpad, position_ids_rmpad, sp_size=self.config.ulysses_sequence_parallel_size
+ )
+
+ # only pass input_ids and position_ids to enable flash_attn_varlen
+ output = self.critic_module(
+ input_ids=input_ids_rmpad,
+ attention_mask=None,
+ position_ids=position_ids_rmpad,
+ **multi_modal_inputs,
+ use_cache=False,
+ ) # prevent model thinks we are generating
+ values_rmpad = output.logits
+ values_rmpad = values_rmpad.squeeze(0) # (total_nnz)
+
+ # gather output if sp > 1
+ if self.config.ulysses_sequence_parallel_size > 1:
+ values_rmpad = gather_outputs_and_unpad(values_rmpad, gather_dim=0, unpad_dim=0, padding_size=pad_size)
+
+ # pad it back
+ values = pad_input(values_rmpad, indices=indices, batch=batch_size, seqlen=seqlen).squeeze(-1)
+ values = values[:, -response_length - 1 : -1]
+ else:
+ output = self.critic_module(
+ input_ids=input_ids,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ **multi_modal_inputs,
+ use_cache=False,
+ )
+ values: torch.Tensor = output.logits
+ values = values[:, -response_length - 1 : -1].squeeze(-1) # (bsz, response_length, vocab_size)
+
+ return values
+
+ def _optimizer_step(self) -> torch.Tensor:
+ if isinstance(self.critic_module, FSDP):
+ grad_norm = self.critic_module.clip_grad_norm_(self.config.max_grad_norm)
+ else:
+ grad_norm = torch.nn.utils.clip_grad_norm_(
+ self.critic_module.parameters(), max_norm=self.config.max_grad_norm
+ )
+
+ if not torch.isfinite(grad_norm):
+ print("Gradient norm is not finite. Skip update.")
+ else:
+ self.critic_optimizer.step()
+
+ self.critic_optimizer.zero_grad()
+ return grad_norm
+
+ @torch.no_grad()
+ def compute_values(self, data: DataProto) -> torch.Tensor:
+ self.critic_module.eval()
+
+ select_keys = ["responses", "input_ids", "attention_mask", "position_ids"]
+ if "multi_modal_inputs" in data.non_tensor_batch.keys():
+ non_tensor_select_keys = ["multi_modal_inputs"]
+ else:
+ non_tensor_select_keys = []
+
+ micro_batches = data.select(select_keys, non_tensor_select_keys).split(
+ self.config.micro_batch_size_per_device_for_experience
+ )
+ values_lst = []
+ if self.rank == 0:
+ micro_batches = tqdm(micro_batches, desc="Compute values", position=2)
+
+ for micro_batch in micro_batches:
+ model_inputs = {**micro_batch.batch, **micro_batch.non_tensor_batch}
+ values = self._forward_micro_batch(model_inputs)
+ values_lst.append(values)
+
+ values = torch.concat(values_lst, dim=0)
+ responses = data.batch["responses"]
+ attention_mask = data.batch["attention_mask"]
+ response_length = responses.size(1)
+ values = values * attention_mask[:, -response_length - 1 : -1]
+ return values
+
+ def update_critic(self, data: DataProto) -> Dict[str, Any]:
+ self.critic_module.train()
+
+ select_keys = ["input_ids", "responses", "attention_mask", "position_ids", "values", "returns"]
+ if "multi_modal_inputs" in data.non_tensor_batch.keys():
+ non_tensor_select_keys = ["multi_modal_inputs"]
+ else:
+ non_tensor_select_keys = []
+
+ # Split to make minibatch iterator for updating the actor
+ # See PPO paper for details. https://arxiv.org/abs/1707.06347
+ mini_batches = data.select(select_keys, non_tensor_select_keys).split(self.config.global_batch_size_per_device)
+
+ metrics = defaultdict(list)
+ for _ in range(self.config.ppo_epochs):
+ if self.rank == 0:
+ mini_batches = tqdm(mini_batches, desc="Train mini-batches", position=2)
+
+ for mini_batch in mini_batches:
+ gradient_accumulation = (
+ self.config.global_batch_size_per_device // self.config.micro_batch_size_per_device_for_update
+ )
+ micro_batches = mini_batch.split(self.config.micro_batch_size_per_device_for_update)
+ if self.rank == 0:
+ micro_batches = tqdm(micro_batches, desc="Update critic", position=3)
+
+ for micro_batch in micro_batches:
+ model_inputs = {**micro_batch.batch, **micro_batch.non_tensor_batch}
+ responses = model_inputs["responses"]
+ attention_mask = model_inputs["attention_mask"]
+ values = model_inputs["values"]
+ returns = model_inputs["returns"]
+ response_length = responses.size(1)
+ action_mask = attention_mask[:, -response_length - 1 : -1] # shift left for value computation
+
+ vpreds = self._forward_micro_batch(model_inputs)
+ vf_loss, vf_clipfrac = core_algos.compute_value_loss(
+ vpreds=vpreds,
+ returns=returns,
+ values=values,
+ action_mask=action_mask,
+ cliprange_value=self.config.cliprange_value,
+ )
+ loss = vf_loss / gradient_accumulation
+ loss.backward()
+
+ batch_metrics = {
+ "critic/vf_loss": vf_loss.detach().item(),
+ "critic/vf_clipfrac": vf_clipfrac.detach().item(),
+ "critic/vpred_mean": VF.masked_mean(vpreds, action_mask).detach().item(),
+ }
+ append_to_dict(metrics, batch_metrics)
+
+ grad_norm = self._optimizer_step()
+ append_to_dict(metrics, {"critic/grad_norm": grad_norm.detach().item()})
+
+ return metrics
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/fsdp_workers.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/fsdp_workers.py
new file mode 100644
index 0000000000000000000000000000000000000000..f62fcccfb4e1414255bd47c417cf9b6f7363e732
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/fsdp_workers.py
@@ -0,0 +1,727 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+The main entry point to run the PPO algorithm
+"""
+
+from typing import Literal, Optional, Union
+
+import numpy as np
+import psutil
+import torch
+import torch.distributed as dist
+from copy import deepcopy
+from accelerate import init_empty_weights
+from codetiming import Timer
+from torch.distributed.device_mesh import init_device_mesh
+from torch.distributed.fsdp import CPUOffload, MixedPrecision, ShardingStrategy
+from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+from transformers import (
+ AutoConfig,
+ AutoModelForCausalLM,
+ AutoModelForTokenClassification,
+ AutoModelForVision2Seq,
+ GenerationConfig,
+ PreTrainedModel,
+)
+from transformers.modeling_utils import no_init_weights
+
+from ..models.monkey_patch import apply_ulysses_patch
+from ..protocol import DataProto
+from ..single_controller.base import Worker
+from ..single_controller.base.decorator import Dispatch, register
+from ..utils.checkpoint.fsdp_checkpoint_manager import FSDPCheckpointManager
+from ..utils.flops_counter import FlopsCounter
+from ..utils.fsdp_utils import (
+ get_fsdp_wrap_policy,
+ get_init_fn,
+ load_fsdp_model,
+ load_fsdp_optimizer,
+ offload_fsdp_model,
+ offload_fsdp_optimizer,
+)
+from ..utils.dataset import process_image
+from ..utils.model_utils import print_gpu_memory_usage, print_model_size
+from ..utils.tokenizer import get_processor, get_tokenizer
+from ..utils.torch_dtypes import PrecisionType
+from ..utils.torch_functional import AnyPrecisionAdamW, get_constant_schedule_with_warmup
+from .config import ActorConfig, CriticConfig, FSDPConfig, ModelConfig, OptimConfig, RefConfig, WorkerConfig
+from .rollout import vLLMRollout
+from .sharding_manager import FSDPVLLMShardingManager
+from .sharding_manager.fsdp_ulysses import FSDPUlyssesShardingManager
+
+
+import torch.nn.functional as F
+_orig_sdp = F.scaled_dot_product_attention
+
+def _safe_sdp(q, k, v, attn_mask=None, **kw):
+ if attn_mask is not None and attn_mask.dtype != q.dtype:
+ attn_mask = attn_mask.to(q.dtype)
+ return _orig_sdp(q, k, v, attn_mask=attn_mask, **kw)
+
+F.scaled_dot_product_attention = _safe_sdp
+
+
+class FSDPWorker(Worker):
+ def __init__(
+ self,
+ config: WorkerConfig,
+ role: Literal["actor", "critic", "rollout", "ref", "actor_rollout", "actor_rollout_ref"],
+ ):
+ super().__init__()
+ self.config = config
+ self.role = role
+
+ if not dist.is_initialized():
+ dist.init_process_group(backend="nccl")
+
+ # improve numerical stability
+ torch.backends.cuda.matmul.allow_tf32 = False
+ torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = False
+
+ self._is_actor = self.role in ["actor", "actor_rollout", "actor_rollout_ref"]
+ self._is_critic = self.role == "critic"
+ self._is_rollout = self.role in ["rollout", "actor_rollout", "actor_rollout_ref"]
+ self._is_ref = self.role in ["ref", "actor_rollout_ref"]
+ self._cache = {}
+
+ self._use_param_offload = False
+ self._use_optimizer_offload = False
+ if self._is_actor:
+ self._use_param_offload = self.config.actor.offload.offload_params
+ self._use_optimizer_offload = self.config.actor.offload.offload_optimizer
+ self._init_config(self.config.actor, "actor")
+ elif self._is_critic:
+ self._use_param_offload = self.config.critic.offload.offload_params
+ self._use_optimizer_offload = self.config.critic.offload.offload_optimizer
+ self._init_config(self.config.critic, "critic")
+ elif self._is_ref: # NOTE: it seems that manual offload is slower than FSDP offload
+ self._use_param_offload = self.config.ref.offload.offload_params
+ self._init_config(self.config.ref, "ref")
+
+ def _init_config(
+ self, config: Union[ActorConfig, CriticConfig, RefConfig], role: Literal["actor", "critic", "ref"]
+ ):
+ world_size = dist.get_world_size()
+ fsdp_size = config.fsdp.fsdp_size
+ if fsdp_size <= 0 or fsdp_size >= world_size:
+ self.device_mesh = init_device_mesh("cuda", mesh_shape=(world_size,), mesh_dim_names=("fsdp",))
+ else: # hsdp
+ self.device_mesh = init_device_mesh(
+ "cuda", mesh_shape=(world_size // fsdp_size, fsdp_size), mesh_dim_names=("ddp", "fsdp")
+ )
+
+ if config.ulysses_sequence_parallel_size > 1:
+ self.ulysses_device_mesh = init_device_mesh(
+ "cuda",
+ mesh_shape=(
+ world_size // config.ulysses_sequence_parallel_size,
+ config.ulysses_sequence_parallel_size,
+ ),
+ mesh_dim_names=("dp", "sp"),
+ )
+ else:
+ self.ulysses_device_mesh = None
+
+ self.ulysses_sharding_manager = FSDPUlyssesShardingManager(self.ulysses_device_mesh)
+
+ if not hasattr(config, "global_batch_size"): # ref model
+ return
+
+ if self.config.rollout.n > 1:
+ config.global_batch_size *= self.config.rollout.n
+ self.print_rank0(f"{role} will use global batch size {config.global_batch_size}.")
+
+ config.global_batch_size_per_device = (
+ config.global_batch_size // self.device_mesh.size() * config.ulysses_sequence_parallel_size
+ )
+ if config.global_batch_size_per_device == 0:
+ raise ValueError(f"{role} global batch size * ulysses size must be larger than num gpus.")
+
+ if config.global_batch_size_per_device % config.micro_batch_size_per_device_for_update != 0:
+ raise ValueError(f"{role} global batch size per device must be divisible by the micro batch size.")
+
+ if (
+ config.fsdp.enable_cpu_offload
+ and config.global_batch_size_per_device != config.micro_batch_size_per_device_for_update
+ ):
+ raise ValueError(f"{role} cannot use FSDP's CPU offload when gradient accumulation is enabled.")
+
+ def _build_model_optimizer(
+ self,
+ model_config: ModelConfig,
+ fsdp_config: FSDPConfig,
+ optim_config: Optional[OptimConfig],
+ padding_free: bool = False,
+ ) -> None:
+ self.tokenizer = get_tokenizer(
+ model_config.tokenizer_path,
+ trust_remote_code=model_config.trust_remote_code,
+ use_fast=True,
+ )
+ self.processor = get_processor(
+ model_config.tokenizer_path,
+ trust_remote_code=model_config.trust_remote_code,
+ use_fast=True,
+ )
+ self.model_config = AutoConfig.from_pretrained(
+ model_config.model_path,
+ trust_remote_code=model_config.trust_remote_code,
+ bos_token_id=self.tokenizer.bos_token_id,
+ eos_token_id=self.tokenizer.eos_token_id,
+ pad_token_id=self.tokenizer.pad_token_id,
+ **model_config.override_config,
+ )
+
+ try:
+ self.generation_config = GenerationConfig.from_pretrained(model_config.model_path)
+ except Exception:
+ self.generation_config = GenerationConfig.from_model_config(self.model_config)
+
+ self.print_rank0(f"Model config: {self.model_config}")
+
+ if padding_free:
+ apply_ulysses_patch(self.model_config.model_type)
+ self.print_rank0("Ulysses patch applied!")
+
+ if fsdp_config.torch_dtype is None:
+ torch_dtype = torch.float32 if self._is_actor or self._is_critic else torch.bfloat16
+ else:
+ torch_dtype = PrecisionType.to_dtype(fsdp_config.torch_dtype)
+
+ if self._is_critic:
+ auto_class = AutoModelForTokenClassification
+ elif type(self.model_config) in AutoModelForVision2Seq._model_mapping.keys():
+ auto_class = AutoModelForVision2Seq
+ else:
+ auto_class = AutoModelForCausalLM
+
+ if (not fsdp_config.enable_rank0_init) or self.device_mesh.get_local_rank("fsdp") == 0:
+ model = auto_class.from_pretrained(
+ model_config.model_path,
+ config=self.model_config,
+ torch_dtype=torch_dtype,
+ attn_implementation="sdpa",
+ # attn_implementation="flash_attention_2",
+ device_map="cpu" if fsdp_config.enable_rank0_init else "cuda",
+ low_cpu_mem_usage=True,
+ trust_remote_code=model_config.trust_remote_code,
+ )
+ else:
+ with no_init_weights(), init_empty_weights():
+ model = auto_class.from_config(
+ self.model_config,
+ torch_dtype=torch_dtype,
+ attn_implementation="sdpa",
+ # attn_implementation="flash_attention_2",
+ trust_remote_code=model_config.trust_remote_code,
+ )
+
+ assert isinstance(model, PreTrainedModel) # lint
+ model.tie_weights() # avoid hanging
+ model = model.to(torch_dtype)
+ if model_config.enable_gradient_checkpointing:
+ model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
+
+ if not (self._is_actor or self._is_critic):
+ model.requires_grad_(False)
+
+ if model_config.freeze_vision_tower:
+ if hasattr(model, "visual"):
+ model.visual.requires_grad_(False)
+ fsdp_config.use_orig_params = True
+ self.print_rank0("Vision tower is set to not trainable.")
+ else:
+ self.print_rank0("No vision tower found.")
+
+ dist.barrier()
+ print_model_size(model)
+ print_gpu_memory_usage("After huggingface model init")
+ mixed_precision = MixedPrecision(
+ param_dtype=PrecisionType.to_dtype(fsdp_config.mp_param_dtype),
+ reduce_dtype=PrecisionType.to_dtype(fsdp_config.mp_reduce_dtype),
+ buffer_dtype=PrecisionType.to_dtype(fsdp_config.mp_buffer_dtype),
+ )
+ auto_wrap_policy = get_fsdp_wrap_policy(model)
+ self.print_rank0(f"FSDP wrap policy: {auto_wrap_policy}.")
+
+ if self.device_mesh.ndim == 2:
+ if fsdp_config.enable_full_shard:
+ sharding_strategy = ShardingStrategy.HYBRID_SHARD
+ else:
+ sharding_strategy = ShardingStrategy._HYBRID_SHARD_ZERO2
+ else:
+ if fsdp_config.enable_full_shard:
+ sharding_strategy = ShardingStrategy.FULL_SHARD
+ else:
+ sharding_strategy = ShardingStrategy.SHARD_GRAD_OP
+
+ if fsdp_config.enable_cpu_offload:
+ cpu_offload = CPUOffload(offload_params=True)
+ else:
+ cpu_offload = None
+
+ if fsdp_config.enable_rank0_init:
+ sync_module_states = True
+ param_init_fn = get_init_fn(model, device="cuda") if self.rank != 0 else None
+ else:
+ sync_module_states = False
+ param_init_fn = None
+
+ self.fsdp_module = FSDP(
+ model,
+ sharding_strategy=sharding_strategy,
+ cpu_offload=cpu_offload,
+ auto_wrap_policy=auto_wrap_policy,
+ mixed_precision=mixed_precision,
+ param_init_fn=param_init_fn,
+ device_id=torch.cuda.current_device(),
+ sync_module_states=sync_module_states,
+ forward_prefetch=False,
+ use_orig_params=fsdp_config.use_orig_params,
+ device_mesh=self.device_mesh,
+ )
+ print_gpu_memory_usage("After FSDP module init")
+
+ if self._is_actor or self._is_critic:
+ if optim_config.strategy == "adamw":
+ self.optimizer = torch.optim.AdamW(
+ filter(lambda p: p.requires_grad, self.fsdp_module.parameters()),
+ lr=optim_config.lr,
+ betas=optim_config.betas,
+ weight_decay=optim_config.weight_decay,
+ fused=True,
+ )
+ elif optim_config.strategy == "adamw_bf16":
+ self.optimizer = AnyPrecisionAdamW(
+ filter(lambda p: p.requires_grad, self.fsdp_module.parameters()),
+ lr=optim_config.lr,
+ betas=optim_config.betas,
+ weight_decay=optim_config.weight_decay,
+ )
+ else:
+ raise NotImplementedError(f"Optimizer {optim_config.strategy} not supported.")
+
+ num_warmup_steps = int(optim_config.lr_warmup_ratio * optim_config.training_steps)
+ self.lr_scheduler = get_constant_schedule_with_warmup(
+ optimizer=self.optimizer, num_warmup_steps=num_warmup_steps
+ )
+ print_gpu_memory_usage("After optimizer init")
+ else:
+ self.optimizer, self.lr_scheduler = None, None
+
+ def _build_rollout(self) -> None:
+ tp_size = self.config.rollout.tensor_parallel_size
+ dp_size = self.world_size // tp_size
+ assert self.world_size % tp_size == 0, (
+ f"rollout world size: {self.world_size} is not divisible by tp size: {tp_size}"
+ )
+ rollout_device_mesh = init_device_mesh("cuda", mesh_shape=(dp_size, tp_size), mesh_dim_names=("dp", "tp"))
+ self.rollout = vLLMRollout(
+ model_path=self.config.actor.model.model_path,
+ config=self.config.rollout,
+ tokenizer=self.tokenizer,
+ )
+ self.rollout_sharding_manager = FSDPVLLMShardingManager(
+ module=self.fsdp_module,
+ inference_engine=self.rollout.inference_engine,
+ device_mesh=rollout_device_mesh,
+ )
+ print_gpu_memory_usage("After vllm init")
+
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
+ def init_model(self):
+ if self._is_critic:
+ model_config = self.config.critic.model
+ fsdp_config = self.config.critic.fsdp
+ optim_config = self.config.critic.optim
+ padding_free = self.config.critic.padding_free
+ role = "critic"
+ elif self._is_actor:
+ model_config = self.config.actor.model
+ fsdp_config = self.config.actor.fsdp
+ optim_config = self.config.actor.optim
+ padding_free = self.config.actor.padding_free
+ role = "actor"
+ elif self._is_ref:
+ model_config = self.config.actor.model
+ fsdp_config = self.config.ref.fsdp
+ optim_config = None
+ padding_free = self.config.ref.padding_free
+ role = "ref"
+ else:
+ raise ValueError(f"Unknown role {role}.")
+
+ if self._is_actor or self._is_critic or self._is_ref:
+ self._build_model_optimizer(
+ model_config=model_config,
+ fsdp_config=fsdp_config,
+ optim_config=optim_config,
+ padding_free=padding_free,
+ )
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+ print_gpu_memory_usage(f"After offload {role} model during init")
+
+ if self._use_optimizer_offload:
+ offload_fsdp_optimizer(optimizer=self.optimizer)
+ print_gpu_memory_usage(f"After offload {role} optimizer during init")
+
+ if self._is_actor:
+ from .actor.dp_actor import DataParallelPPOActor # lazy import
+
+ self.actor = DataParallelPPOActor(
+ config=self.config.actor,
+ actor_module=self.fsdp_module,
+ actor_optimizer=self.optimizer,
+ )
+
+ if self._is_critic:
+ from .critic.dp_critic import DataParallelPPOCritic # lazy import
+
+ self.critic = DataParallelPPOCritic(
+ config=self.config,
+ critic_module=self.fsdp_module,
+ critic_optimizer=self.optimizer,
+ )
+
+ if self._is_rollout:
+ self._build_rollout()
+
+ if self._is_ref:
+ from .actor.dp_actor import DataParallelPPOActor # lazy import
+
+ self.ref_policy = DataParallelPPOActor(
+ config=self.config.ref,
+ actor_module=self.fsdp_module,
+ )
+
+ if self._is_actor or self._is_critic:
+ self.flops_counter = FlopsCounter(self.model_config)
+ self.checkpoint_manager = FSDPCheckpointManager(
+ model=self.fsdp_module,
+ optimizer=self.optimizer,
+ lr_scheduler=self.lr_scheduler,
+ processing_class=self.processor if self.processor is not None else self.tokenizer,
+ )
+
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
+ def save_checkpoint(self, path: str):
+ assert self._is_actor or self._is_critic
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ self.checkpoint_manager.save_checkpoint(path)
+ dist.barrier()
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
+ def load_checkpoint(self, path: str):
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ self.checkpoint_manager.load_checkpoint(path)
+ dist.barrier()
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ if self._use_optimizer_offload: # avoid OOM in resuming
+ offload_fsdp_optimizer(self.optimizer)
+
+ def preprocess_multi_modal_data(self, data: DataProto):
+ # inplace load & process image data
+ min_pixels = data.meta_info["min_pixels"]
+ max_pixels = data.meta_info["max_pixels"]
+ multi_modal_data_copy = deepcopy(data.non_tensor_batch["multi_modal_data"])
+
+ processed_images = []
+ for multi_modal_data in multi_modal_data_copy:
+ processed_per_query_images = []
+ for image in multi_modal_data['image']:
+ processed_per_query_images.append(
+ process_image(image, min_pixels=min_pixels, max_pixels=max_pixels)
+ )
+ processed_images.append(processed_per_query_images)
+
+ # Note: Using the alternative (commented) code below to process images can lead to subtle resize issues:
+ # For example: an image with size (656, 369) should be resized to (682, 383) with `min_pixels=512 ** 2`,
+ # however, it will produce an image with size (683, 383) when using the following for loop.
+ # (But it works normally when directly applying `process_image` to this image).
+ # This behavior is unexpected and difficult to explain.
+ # The code above works normally.
+
+ # images = [multi_modal_data['image'] for multi_modal_data in multi_modal_data_copy]
+ # for i, per_query_images in enumerate(images):
+ # for j, image in enumerate(per_query_images):
+ # images[i][j] = process_image(image, min_pixels=min_pixels, max_pixels=max_pixels)
+
+ multi_modal_inputs = np.array([
+ # dict(self.processor.image_processor(images=per_query_images, videos=None))
+ dict(self.processor.image_processor(images=per_query_images))
+ for per_query_images in processed_images
+ ], dtype=object)
+
+ data.non_tensor_batch["multi_modal_inputs"] = multi_modal_inputs
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)
+ def update_actor(self, data: DataProto):
+ assert self._is_actor
+ if "multi_modal_inputs" in self._cache:
+ data.non_tensor_batch['multi_modal_inputs'] = deepcopy(self._cache['multi_modal_inputs'])
+ elif "multi_modal_data" in data.non_tensor_batch:
+ self.preprocess_multi_modal_data(data)
+
+ data = data.to(torch.cuda.current_device())
+
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ if self._use_optimizer_offload:
+ load_fsdp_optimizer(optimizer=self.optimizer)
+
+ with self.ulysses_sharding_manager:
+ data = self.ulysses_sharding_manager.preprocess_data(data=data)
+ with Timer(name="update_policy", logger=None) as timer:
+ metrics = self.actor.update_policy(data=data)
+
+ delta_time = timer.last
+ global_num_tokens = data.meta_info["global_token_num"]
+ estimated_flops, promised_flops = self.flops_counter.estimate_flops(global_num_tokens, delta_time)
+ metrics["perf/mfu_actor"] = (
+ estimated_flops * self.config.actor.ppo_epochs / (promised_flops * self.world_size)
+ )
+ metrics["perf/max_memory_allocated_gb"] = (
+ torch.cuda.max_memory_allocated() - self.rollout_sharding_manager.freed_bytes
+ ) / (1024**3)
+ metrics["perf/max_memory_reserved_gb"] = (
+ torch.cuda.max_memory_reserved() - self.rollout_sharding_manager.freed_bytes
+ ) / (1024**3)
+ metrics["perf/cpu_memory_used_gb"] = psutil.virtual_memory().used / (1024**3)
+
+ self.lr_scheduler.step()
+ lr = self.lr_scheduler.get_last_lr()[0]
+ metrics["actor/lr"] = lr
+
+ # Metrics should be in non_tensor_batch instead of meta_info, as DataProto not concat meta_info.
+ output = DataProto(
+ non_tensor_batch={
+ key: np.array([value] if np.isscalar(value) else value) for key, value in metrics.items()
+ }
+ )
+
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ if self._use_optimizer_offload:
+ offload_fsdp_optimizer(optimizer=self.optimizer)
+
+ output = output.to("cpu")
+ return output
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)
+ def generate_sequences(self, prompts: DataProto):
+ assert self._is_rollout
+
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ meta_info = {
+ "eos_token_id": self.generation_config.eos_token_id
+ if self.generation_config is not None
+ else self.tokenizer.eos_token_id,
+ "pad_token_id": self.generation_config.pad_token_id
+ if self.generation_config is not None
+ else self.tokenizer.pad_token_id,
+ }
+ prompts.meta_info.update(meta_info)
+ with self.rollout_sharding_manager:
+ # after parameters sync with rollout, offload actor model to CPU
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ if self._use_optimizer_offload:
+ offload_fsdp_optimizer(optimizer=self.optimizer)
+
+ prompts = self.rollout_sharding_manager.preprocess_data(prompts)
+
+ # load image data
+ cached_multi_modal_data = None
+ if "multi_modal_data" in prompts.non_tensor_batch:
+ cached_multi_modal_data = deepcopy(prompts.non_tensor_batch["multi_modal_data"])
+ min_pixels = prompts.meta_info['min_pixels']
+ max_pixels = prompts.meta_info['max_pixels']
+ processed_images = []
+ for i, multi_modal_data in enumerate(prompts.non_tensor_batch["multi_modal_data"]):
+ for j, image in enumerate(multi_modal_data["image"]):
+ multi_modal_data['image'][j] = process_image(image, min_pixels=min_pixels, max_pixels=max_pixels)
+ processed_images.append(multi_modal_data)
+ prompts.non_tensor_batch["multi_modal_data"] = processed_images
+
+ output = self.rollout.generate_sequences(prompts=prompts)
+
+ if cached_multi_modal_data is not None:
+ sampling_n = prompts.meta_info.get("n", self.config.rollout.n)
+ # restore multi_modal_data
+ output.non_tensor_batch["multi_modal_data"] = cached_multi_modal_data
+ if sampling_n > 1:
+ output.non_tensor_batch["multi_modal_data"] = np.repeat(
+ output.non_tensor_batch["multi_modal_data"], repeats=sampling_n, axis=0,
+ )
+
+ output = self.rollout_sharding_manager.postprocess_data(output)
+
+ output = output.to("cpu")
+ return output
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)
+ def compute_log_probs(self, data: DataProto):
+ assert self._is_actor
+ self._cache.clear()
+ if "multi_modal_data" in data.non_tensor_batch:
+ self.preprocess_multi_modal_data(data)
+ # create cache for multi_modal_inputs
+ self._cache['multi_modal_inputs'] = deepcopy(data.non_tensor_batch['multi_modal_inputs'])
+
+ data = data.to(torch.cuda.current_device())
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ # we should always recompute old_log_probs when it is HybridEngine
+ data.meta_info["temperature"] = self.config.rollout.temperature
+ # perform recompute log_prob
+ with self.ulysses_sharding_manager:
+ data = self.ulysses_sharding_manager.preprocess_data(data)
+ output = self.actor.compute_log_prob(data=data)
+ output = DataProto.from_dict(
+ tensors={"old_log_probs": output}, meta_info={"temperature": self.config.rollout.temperature}
+ )
+ output = self.ulysses_sharding_manager.postprocess_data(output)
+
+ # https://pytorch.org/docs/stable/notes/fsdp.html#fsdp-notes
+ # unshard the root FSDP module
+ if self.world_size > 1:
+ self.fsdp_module._handle.reshard(True)
+
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ output = output.to("cpu")
+ return output
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)
+ def compute_ref_log_probs(self, data: DataProto):
+ # The `self._cache` is empty here since cached `multi_modal_inputs` is only saved in the actor's _cache,
+ # not in the ref_policy's or critic's caches.
+ assert self._is_ref
+ if "multi_modal_inputs" in self._cache:
+ data.non_tensor_batch['multi_modal_inputs'] = deepcopy(self._cache['multi_modal_inputs'])
+ elif "multi_modal_data" in data.non_tensor_batch:
+ self.preprocess_multi_modal_data(data)
+
+ data = data.to(torch.cuda.current_device())
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ data.meta_info["temperature"] = self.config.rollout.temperature
+ with self.ulysses_sharding_manager:
+ data = self.ulysses_sharding_manager.preprocess_data(data)
+ output = self.ref_policy.compute_log_prob(data=data)
+ output = DataProto.from_dict(tensors={"ref_log_probs": output})
+ output = self.ulysses_sharding_manager.postprocess_data(output)
+
+ # https://pytorch.org/docs/stable/notes/fsdp.html#fsdp-notes
+ # unshard the root FSDP module
+ if self.world_size > 1:
+ self.fsdp_module._handle.reshard(True)
+
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ output = output.to("cpu")
+ return output
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)
+ def compute_values(self, data: DataProto):
+ assert self._is_critic
+ # The `self._cache` is empty here since cached `multi_modal_inputs` is only saved in the actor's _cache,
+ # not in the ref_policy's or critic's caches.
+ if "multi_modal_inputs" in self._cache:
+ data.non_tensor_batch['multi_modal_inputs'] = deepcopy(self._cache['multi_modal_inputs'])
+ elif "multi_modal_data" in data.non_tensor_batch:
+ self.preprocess_multi_modal_data(data)
+
+ data = data.to(torch.cuda.current_device())
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ with self.ulysses_sharding_manager:
+ data = self.ulysses_sharding_manager.preprocess_data(data=data)
+ values = self.critic.compute_values(data=data)
+ output = DataProto.from_dict(tensors={"values": values})
+ output = self.ulysses_sharding_manager.postprocess_data(data=output)
+
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ output = output.to("cpu")
+ return output
+
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)
+ def update_critic(self, data: DataProto):
+ # The `self._cache` is empty here since cached `multi_modal_inputs` is only saved in the actor's _cache,
+ # not in the ref_policy's or critic's caches.
+ if "multi_modal_inputs" in self._cache:
+ data.non_tensor_batch['multi_modal_inputs'] = deepcopy(self._cache['multi_modal_inputs'])
+ elif "multi_modal_data" not in data.non_tensor_batch:
+ self.preprocess_multi_modal_data(data)
+
+ data = data.to(torch.cuda.current_device())
+ if self._use_param_offload:
+ load_fsdp_model(self.fsdp_module)
+
+ if self._use_optimizer_offload:
+ load_fsdp_optimizer(optimizer=self.optimizer)
+
+ with self.ulysses_sharding_manager:
+ data = self.ulysses_sharding_manager.preprocess_data(data=data)
+ with Timer(name="update_critic", logger=None) as timer:
+ metrics = self.critic.update_critic(data=data)
+
+ delta_time = timer.last
+ global_num_tokens = data.meta_info["global_token_num"]
+ estimated_flops, promised_flops = self.flops_counter.estimate_flops(global_num_tokens, delta_time)
+ metrics["perf/mfu_critic"] = (
+ estimated_flops * self.config.actor.ppo_epochs / (promised_flops * self.world_size)
+ )
+
+ self.lr_scheduler.step()
+ lr = self.lr_scheduler.get_last_lr()[0]
+ metrics["critic/lr"] = lr
+
+ # Metrics should be in non_tensor_batch instead of meta_info, as DataProto not concat meta_info.
+ output = DataProto(
+ non_tensor_batch={
+ metric: np.array([value] if np.isscalar(value) else value) for metric, value in metrics.items()
+ }
+ )
+
+ if self._use_param_offload:
+ offload_fsdp_model(self.fsdp_module)
+
+ if self._use_optimizer_offload:
+ offload_fsdp_optimizer(optimizer=self.optimizer)
+
+ output = output.to("cpu")
+ return output
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d476f6be089689a08e7587cf8985169ed401730
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2024 PRIME team and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .config import RewardConfig
+from .function import BatchFunctionRewardManager, FunctionRewardManager, SequentialFunctionRewardManager
+
+
+__all__ = ["BatchFunctionRewardManager", "FunctionRewardManager", "RewardConfig", "SequentialFunctionRewardManager"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/config.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e11bdb2de07a8a91a18a41534dcba71817937ed
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/config.py
@@ -0,0 +1,43 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Reward config
+"""
+
+import os
+from dataclasses import dataclass, field
+from typing import Optional
+
+
+@dataclass
+class RewardConfig:
+ reward_type: str = "batch"
+ reward_function: Optional[str] = None
+ reward_function_kwargs: dict = field(default_factory=dict)
+ skip_special_tokens: bool = True
+ num_cpus: int = 1
+ """auto keys"""
+ reward_function_name: Optional[str] = field(default=None, init=False)
+
+ def post_init(self):
+ if self.reward_function is not None: # support custom reward function, e.g., ./math.py:main
+ if ":" not in self.reward_function:
+ self.reward_function_name = "main"
+ else:
+ self.reward_function, self.reward_function_name = self.reward_function.rsplit(":", maxsplit=1)
+
+ if os.path.exists(self.reward_function): # ray job uses absolute path
+ self.reward_function = os.path.abspath(self.reward_function)
+ else:
+ self.reward_function = None
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/function.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/function.py
new file mode 100644
index 0000000000000000000000000000000000000000..a7af022ee7a90543c78a4864a515405b33ab9a19
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/reward/function.py
@@ -0,0 +1,119 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib.util
+import os
+import sys
+from abc import ABC, abstractmethod
+from collections import defaultdict
+from functools import partial
+from typing import Callable, Dict, List, Optional, Tuple, TypedDict
+
+import torch
+from transformers import PreTrainedTokenizer
+
+from ...protocol import DataProto
+from .config import RewardConfig
+
+
+class RewardScore(TypedDict):
+ overall: float
+ format: Optional[float]
+ accuracy: Optional[float]
+
+
+SequentialRewardFunction = Callable[[str, str], RewardScore]
+
+BatchRewardFunction = Callable[[List[str], List[str]], List[RewardScore]]
+
+
+class FunctionRewardManager(ABC):
+ """Reward manager for rule-based reward."""
+
+ def __init__(self, config: RewardConfig, tokenizer: PreTrainedTokenizer):
+ if config.reward_function is None:
+ raise ValueError("Reward function is not provided.")
+
+ if not os.path.exists(config.reward_function):
+ raise FileNotFoundError(f"Reward function file {config.reward_function} not found.")
+
+ spec = importlib.util.spec_from_file_location("custom_reward_fn", config.reward_function)
+ module = importlib.util.module_from_spec(spec)
+ try:
+ sys.modules["custom_reward_fn"] = module
+ spec.loader.exec_module(module)
+ except Exception as e:
+ raise RuntimeError(f"Failed to load reward function: {e}")
+
+ if not hasattr(module, config.reward_function_name):
+ raise AttributeError(f"Module {module} does not have function {config.reward_function_name}.")
+
+ reward_fn = getattr(module, config.reward_function_name)
+ print(f"Using reward function `{config.reward_function_name}` from `{config.reward_function}`.")
+ self.reward_fn = partial(reward_fn, **config.reward_function_kwargs)
+ self.config = config
+ self.tokenizer = tokenizer
+
+ @abstractmethod
+ def compute_reward(self, data: DataProto) -> Tuple[torch.Tensor, Dict[str, List[float]]]:
+ """Compute reward for a batch of data."""
+ ...
+
+
+class SequentialFunctionRewardManager(FunctionRewardManager):
+ reward_fn: SequentialRewardFunction
+
+ def compute_reward(self, data: DataProto) -> Tuple[torch.Tensor, Dict[str, List[float]]]:
+ reward_tensor = torch.zeros_like(data.batch["responses"], dtype=torch.float32)
+ reward_metrics = defaultdict(list)
+ response_ids = data.batch["responses"]
+ response_length = data.batch["response_mask"].sum(dim=-1)
+ for i in range(len(data)):
+ valid_response_ids = response_ids[i][: response_length[i]]
+ response_str = self.tokenizer.decode(
+ valid_response_ids, skip_special_tokens=self.config.skip_special_tokens
+ )
+ ground_truth = data.non_tensor_batch["ground_truth"][i]
+
+ score = self.reward_fn(response_str, ground_truth)
+ reward_tensor[i, response_length[i] - 1] = score["overall"]
+ for key, value in score.items():
+ reward_metrics[key].append(value)
+
+ return reward_tensor, reward_metrics
+
+
+class BatchFunctionRewardManager(FunctionRewardManager):
+ reward_fn: BatchRewardFunction
+
+ def compute_reward(self, data: DataProto) -> Tuple[torch.Tensor, Dict[str, List[float]]]:
+ response_str, ground_truth = [], []
+ response_ids = data.batch["responses"]
+ response_length = data.batch["response_mask"].sum(dim=-1)
+ for i in range(len(data)):
+ valid_response_ids = response_ids[i][: response_length[i]]
+ response_str.append(
+ self.tokenizer.decode(valid_response_ids, skip_special_tokens=self.config.skip_special_tokens)
+ )
+ ground_truth.append(data.non_tensor_batch["ground_truth"][i])
+
+ scores = self.reward_fn(response_str, ground_truth)
+ reward_tensor = torch.zeros_like(data.batch["responses"], dtype=torch.float32)
+ reward_metrics = defaultdict(list)
+ for i, score in enumerate(scores):
+ reward_tensor[i, response_length[i] - 1] = score["overall"]
+ for key, value in score.items():
+ reward_metrics[key].append(value)
+
+ return reward_tensor, reward_metrics
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b294bcea67d0caae8e4cba6c99dcfd8221440f7a
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/__init__.py
@@ -0,0 +1,20 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .config import RolloutConfig
+from .vllm_rollout_spmd import vLLMRollout
+
+
+__all__ = ["RolloutConfig", "vLLMRollout"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/base.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a07eeedc844a23ce153771c880124f560f14326
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/base.py
@@ -0,0 +1,27 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from abc import ABC, abstractmethod
+
+from ...protocol import DataProto
+
+
+__all__ = ["BaseRollout"]
+
+
+class BaseRollout(ABC):
+ @abstractmethod
+ def generate_sequences(self, prompts: DataProto) -> DataProto:
+ """Generate sequences"""
+ pass
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/config.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4c96ca355ad3de9262a07f8e66d2c738012b400
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/config.py
@@ -0,0 +1,47 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Rollout config
+"""
+
+from dataclasses import asdict, dataclass, field
+from typing import Any, Dict, Optional
+
+
+@dataclass
+class RolloutConfig:
+ name: str = "vllm"
+ n: int = 1
+ temperature: float = 1.0
+ top_p: float = 1.0
+ top_k: int = -1
+ seed: int = 1
+ limit_images: int = 0
+ dtype: str = "bf16"
+ gpu_memory_utilization: float = 0.6
+ ignore_eos: bool = False
+ enforce_eager: bool = False
+ enable_chunked_prefill: bool = False # only for v0 engine
+ tensor_parallel_size: int = 2
+ max_model_len: Optional[int] = None
+ max_num_batched_tokens: int = 8192
+ disable_log_stats: bool = True
+ val_override_config: Dict[str, Any] = field(default_factory=dict)
+ """auto keys"""
+ prompt_length: int = field(default=-1, init=False)
+ response_length: int = field(default=-1, init=False)
+ trust_remote_code: bool = field(default=False, init=False)
+
+ def to_dict(self):
+ return asdict(self)
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/vllm_rollout_spmd.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/vllm_rollout_spmd.py
new file mode 100644
index 0000000000000000000000000000000000000000..22a8db6916dc9b6f988af47ddb6f4c65ef3a4a3c
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/rollout/vllm_rollout_spmd.py
@@ -0,0 +1,206 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from contextlib import contextmanager
+from typing import Any, Dict, List, Optional, Union
+
+import numpy as np
+import torch
+import torch.distributed
+from tensordict import TensorDict
+from transformers import PreTrainedTokenizer
+from vllm import LLM, RequestOutput, SamplingParams
+
+from ...protocol import DataProto
+from ...utils import torch_functional as VF
+from ...utils.tokenizer import get_processor
+from ...utils.torch_dtypes import PrecisionType
+from .base import BaseRollout
+from .config import RolloutConfig
+
+
+def _repeat_interleave(value: Union[torch.Tensor, np.ndarray], repeats: int) -> Union[torch.Tensor, List[Any]]:
+ if isinstance(value, torch.Tensor):
+ return value.repeat_interleave(repeats, dim=0)
+ else:
+ return np.repeat(value, repeats, axis=0)
+
+
+def _get_logit_bias(model_path: str, trust_remote_code: bool) -> Optional[Dict[int, float]]:
+ processor = get_processor(model_path, trust_remote_code=trust_remote_code)
+ # if processor is not None and hasattr(processor, "image_token"):
+ # image_token_id = processor.tokenizer.convert_tokens_to_ids(processor.image_token)
+ # return {image_token_id: -100}
+ # else:
+ return None
+
+
+class vLLMRollout(BaseRollout):
+ def __init__(self, model_path: str, config: RolloutConfig, tokenizer: PreTrainedTokenizer):
+ """A vLLM rollout. It requires the module is supported by the vllm.
+
+ Args:
+ module: module here follows huggingface APIs
+ config: DictConfig
+ tokenizer: the task/model tokenizer
+ """
+ super().__init__()
+ self.rank = int(os.getenv("RANK", "0"))
+ self.config = config
+ self.pad_token_id = tokenizer.pad_token_id
+ if config.tensor_parallel_size > torch.distributed.get_world_size():
+ raise ValueError("Tensor parallelism size should be less than world size.")
+
+ if config.max_num_batched_tokens < config.prompt_length + config.response_length:
+ raise ValueError("max_num_batched_tokens should be greater than prompt_length + response_length. {config.prompt_length + config.response_length}")
+
+ engine_kwargs = {}
+ if config.limit_images:
+ engine_kwargs["limit_mm_per_prompt"] = {"image": config.limit_images}
+
+ self.inference_engine = LLM(
+ model=model_path,
+ skip_tokenizer_init=False,
+ trust_remote_code=config.trust_remote_code,
+ load_format="dummy",
+ dtype=PrecisionType.to_str(PrecisionType.to_dtype(config.dtype)),
+ seed=config.seed,
+ max_num_seqs=4,
+ max_model_len=config.max_model_len or config.prompt_length + config.response_length,
+ distributed_executor_backend="external_launcher",
+ tensor_parallel_size=config.tensor_parallel_size,
+ gpu_memory_utilization=config.gpu_memory_utilization,
+ max_num_batched_tokens=config.max_num_batched_tokens,
+ disable_log_stats=config.disable_log_stats,
+ enforce_eager=config.enforce_eager,
+ disable_custom_all_reduce=True,
+ disable_mm_preprocessor_cache=True,
+ enable_chunked_prefill=config.enable_chunked_prefill,
+ enable_sleep_mode=True,
+ **engine_kwargs,
+ )
+
+ # Offload vllm model to reduce peak memory usage
+ self.inference_engine.sleep(level=1)
+
+ sampling_kwargs = {
+ "max_tokens": config.response_length,
+ "detokenize": False,
+ "logit_bias": _get_logit_bias(model_path, trust_remote_code=config.trust_remote_code),
+ }
+ default_sampling_params = SamplingParams()
+ for key in config.to_dict().keys():
+ if hasattr(default_sampling_params, key):
+ sampling_kwargs[key] = getattr(config, key)
+
+ print(f"Sampling params: {sampling_kwargs}.")
+ self.sampling_params = SamplingParams(**sampling_kwargs)
+
+ @contextmanager
+ def update_sampling_params(self, **kwargs):
+ # update sampling params
+ old_sampling_params_args = {}
+ if kwargs:
+ for key, value in kwargs.items():
+ if hasattr(self.sampling_params, key):
+ old_value = getattr(self.sampling_params, key)
+ old_sampling_params_args[key] = old_value
+ setattr(self.sampling_params, key, value)
+
+ yield
+ # roll back to previous sampling params
+ for key, value in old_sampling_params_args.items():
+ setattr(self.sampling_params, key, value)
+
+ @torch.no_grad()
+ def generate_sequences(self, prompts: DataProto) -> DataProto:
+ if self.rank == 0:
+ print("[Rollout] Start generating sequences.")
+
+ # left-padded attention_mask
+ input_ids: torch.Tensor = prompts.batch["input_ids"] # (bs, prompt_length)
+ attention_mask: torch.Tensor = prompts.batch["attention_mask"]
+ position_ids: torch.Tensor = prompts.batch["position_ids"]
+ eos_token_id: int = prompts.meta_info["eos_token_id"]
+ batch_size = input_ids.size(0)
+
+ non_tensor_batch = prompts.non_tensor_batch
+ if batch_size != len(non_tensor_batch["raw_prompt_ids"]):
+ raise RuntimeError("vllm sharding manager is not work properly.")
+
+ if "multi_modal_data" in non_tensor_batch:
+ vllm_inputs = []
+ for raw_prompt_ids, multi_modal_data in zip(
+ non_tensor_batch.pop("raw_prompt_ids"), non_tensor_batch.pop("multi_modal_data")
+ ):
+ vllm_inputs.append({"prompt_token_ids": list(raw_prompt_ids), "multi_modal_data": multi_modal_data})
+ else:
+ vllm_inputs = [
+ {"prompt_token_ids": list(raw_prompt_ids)} for raw_prompt_ids in non_tensor_batch.pop("raw_prompt_ids")
+ ]
+
+ # users can customize different sampling_params at different run
+ with self.update_sampling_params(**prompts.meta_info):
+ completions: List[RequestOutput] = self.inference_engine.generate(
+ prompts=vllm_inputs, sampling_params=self.sampling_params, use_tqdm=False
+ )
+ response_ids = [output.token_ids for completion in completions for output in completion.outputs]
+ response_ids = VF.pad_2d_list_to_length(
+ response_ids, self.pad_token_id, max_length=self.config.response_length
+ ).to(input_ids.device)
+
+ if self.sampling_params.n > 1:
+ batch_size = batch_size * self.sampling_params.n
+ input_ids = _repeat_interleave(input_ids, self.sampling_params.n)
+ attention_mask = _repeat_interleave(attention_mask, self.sampling_params.n)
+ position_ids = _repeat_interleave(position_ids, self.sampling_params.n)
+
+ sequence_ids = torch.cat([input_ids, response_ids], dim=-1)
+ response_length = response_ids.size(1)
+ delta_position_id = torch.arange(1, response_length + 1, device=position_ids.device)
+ delta_position_id = delta_position_id.view(1, -1).expand(batch_size, -1)
+ if position_ids.dim() == 3: # qwen2vl mrope
+ delta_position_id = delta_position_id.view(batch_size, 1, -1).expand(batch_size, 3, -1)
+
+ # prompt: left pad + response: right pad
+ # attention_mask: [0,0,0,0,1,1,1,1 | 1,1,1,0,0,0,0,0]
+ # position_ids: [0,0,0,0,0,1,2,3 | 4,5,6,7,8,9,10,11]
+ response_position_ids = position_ids[..., -1:] + delta_position_id
+ position_ids = torch.cat([position_ids, response_position_ids], dim=-1)
+ response_mask = VF.get_response_mask(
+ response_ids=response_ids, eos_token_id=eos_token_id, dtype=attention_mask.dtype
+ )
+ attention_mask = torch.cat((attention_mask, response_mask), dim=-1)
+
+ # all the tp ranks should contain the same data here. data in all ranks are valid
+ batch = TensorDict(
+ {
+ "prompts": input_ids,
+ "responses": response_ids,
+ "input_ids": sequence_ids, # here input_ids become the whole sentences
+ "attention_mask": attention_mask,
+ "response_mask": response_mask,
+ "position_ids": position_ids,
+ },
+ batch_size=batch_size,
+ )
+ if self.rank == 0:
+ print("[Rollout] Finish generating sequences.")
+
+ return DataProto(
+ batch=batch,
+ non_tensor_batch=non_tensor_batch,
+ meta_info=prompts.meta_info.copy(),
+ )
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/__init__.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..88eaee49c4bba4654b3195ae4a78728407953793
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/__init__.py
@@ -0,0 +1,21 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .base import BaseShardingManager
+from .fsdp_ulysses import FSDPUlyssesShardingManager
+from .fsdp_vllm import FSDPVLLMShardingManager
+
+
+__all__ = ["BaseShardingManager", "FSDPUlyssesShardingManager", "FSDPVLLMShardingManager"]
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/base.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc2975615100c8ce6af853ef5c39cc67e94f7e5d
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/base.py
@@ -0,0 +1,32 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Sharding manager to implement HybridEngine
+"""
+
+from ...protocol import DataProto
+
+
+class BaseShardingManager:
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ pass
+
+ def preprocess_data(self, data: DataProto) -> DataProto:
+ return data
+
+ def postprocess_data(self, data: DataProto) -> DataProto:
+ return data
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/fsdp_ulysses.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/fsdp_ulysses.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2ce5b9586ae41faa79590a7d7e5f34b603683de
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/fsdp_ulysses.py
@@ -0,0 +1,65 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Contains a resharding manager that binds weights from FSDP zero3 to XPerfGPT
+"""
+
+from torch.distributed.device_mesh import DeviceMesh
+
+from ...protocol import DataProto, all_gather_data_proto
+from ...utils.ulysses import get_ulysses_sequence_parallel_group, set_ulysses_sequence_parallel_group
+from .base import BaseShardingManager
+
+
+class FSDPUlyssesShardingManager(BaseShardingManager):
+ """
+ Sharding manager to support data resharding when using FSDP + Ulysses
+ """
+
+ def __init__(self, device_mesh: DeviceMesh):
+ super().__init__()
+ self.device_mesh = device_mesh
+
+ def __enter__(self):
+ if self.device_mesh is not None:
+ self.prev_sp_group = get_ulysses_sequence_parallel_group()
+ set_ulysses_sequence_parallel_group(self.device_mesh["sp"].get_group())
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self.device_mesh is not None:
+ set_ulysses_sequence_parallel_group(self.prev_sp_group)
+
+ def preprocess_data(self, data: DataProto) -> DataProto:
+ """
+ AllGather data from sp region
+ This is because the data is first sharded along the FSDP dimension as we utilize the DP_COMPUTE
+ In Ulysses, we need to make sure the same data is used across a SP group
+ """
+ if self.device_mesh is not None:
+ sp_size = self.device_mesh["sp"].size()
+ sp_group = self.device_mesh["sp"].get_group()
+ all_gather_data_proto(data, size=sp_size, group=sp_group)
+
+ return data
+
+ def postprocess_data(self, data: DataProto) -> DataProto:
+ """
+ Split the data to follow FSDP partition
+ """
+ if self.device_mesh is not None:
+ sp_size = self.device_mesh["sp"].size()
+ sp_rank = self.device_mesh["sp"].get_local_rank()
+ data = data.chunk(chunks=sp_size)[sp_rank]
+
+ return data
diff --git a/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/fsdp_vllm.py b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/fsdp_vllm.py
new file mode 100644
index 0000000000000000000000000000000000000000..11f10903d5d5860d0dc87a8c2a09bdd4e7188ef4
--- /dev/null
+++ b/LLaVA-NeXT/llava-critic-r1/EasyR1/verl/workers/sharding_manager/fsdp_vllm.py
@@ -0,0 +1,149 @@
+# Copyright 2024 Bytedance Ltd. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import inspect
+import re
+from typing import Dict, Iterable, Tuple, Union
+
+import torch
+import torch.distributed as dist
+from torch.distributed._tensor import DTensor
+from torch.distributed.checkpoint.state_dict import get_model_state_dict
+from torch.distributed.device_mesh import DeviceMesh
+from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP
+from transformers import PreTrainedModel
+from vllm import LLM
+from vllm.distributed import parallel_state as vllm_ps
+
+from ...protocol import DataProto, all_gather_data_proto
+from ...utils.model_utils import print_gpu_memory_usage
+from .base import BaseShardingManager
+
+
+class FSDPVLLMShardingManager(BaseShardingManager):
+ def __init__(
+ self,
+ module: FSDP,
+ inference_engine: LLM,
+ device_mesh: DeviceMesh,
+ ):
+ self.module = module
+ self.inference_engine = inference_engine
+ self.device_mesh = device_mesh
+
+ self.world_size = dist.get_world_size()
+ self.tp_size = vllm_ps.get_tensor_model_parallel_world_size()
+ self.tp_rank = vllm_ps.get_tensor_model_parallel_rank()
+ self.tp_group = vllm_ps.get_tensor_model_parallel_group().device_group
+
+ # Record freed bytes to estimate memory usage correctly
+ # https://github.com/vllm-project/vllm/pull/11743#issuecomment-2754338119
+ self.freed_bytes = 0
+
+ # Note that torch_random_states may be different on each dp rank
+ self.torch_random_states = torch.cuda.get_rng_state()
+ # get a random rng states
+ gen_dp_rank = self.device_mesh["dp"].get_local_rank()
+ torch.cuda.manual_seed(gen_dp_rank + 1000) # make sure all tp ranks have the same random states
+ self.gen_random_states = torch.cuda.get_rng_state()
+ torch.cuda.set_rng_state(self.torch_random_states)
+
+ def _rename_weight_keys(self, actor_weights: Dict[str, Union[torch.Tensor, DTensor]], model: PreTrainedModel):
+ # convert state dict keys: https://github.com/huggingface/transformers/pull/38385
+ if not hasattr(model, "_checkpoint_conversion_mapping"):
+ return actor_weights
+
+ reverse_key_mapping = {v: k for k, v in model._checkpoint_conversion_mapping.items()}
+ original_weights = {}
+ for key, value in actor_weights.items():
+ for pattern, replacement in reverse_key_mapping.items():
+ replacement = replacement.lstrip("^") # strip off un-needed chars and patterns
+ replacement = re.sub(r"\(.*\)", "", replacement)
+ key, n_replace = re.subn(pattern, replacement, key)
+ # Early exit of the loop
+ if n_replace > 0:
+ break
+
+ original_weights[key] = value
+
+ return original_weights
+
+ def _make_weight_iterator(
+ self, actor_weights: Dict[str, Union[torch.Tensor, DTensor]]
+ ) -> Iterable[Tuple[str, torch.Tensor]]:
+ for name, tensor in actor_weights.items():
+ yield name, tensor.full_tensor() if self.world_size != 1 else tensor
+
+ def __enter__(self):
+ # NOTE: Basically, we only need `torch.cuda.empty_cache()` before vllm wake_up and
+ # after vllm sleep, since vllm has its own caching memory allocator CuMemAllocator.
+ # Out of vllm scope, we should avoid empty cache to let pytorch using caching memory
+ # to speed up memory allocations.
+ #
+ # pytorch: https://pytorch.org/docs/stable/notes/cuda.html#memory-management
+ # vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/device_allocator/cumem.py#L103
+ torch.cuda.empty_cache()
+ print_gpu_memory_usage("Before state_dict() in sharding manager")
+ actor_weights = get_model_state_dict(self.module)
+ actor_weights = self._rename_weight_keys(actor_weights, self.module._fsdp_wrapped_module)
+ print_gpu_memory_usage("After state_dict() in sharding manager")
+
+ if "tags" in inspect.signature(self.inference_engine.wake_up).parameters:
+ self.inference_engine.wake_up(tags=["weights"])
+ else:
+ self.inference_engine.wake_up()
+
+ model = self.inference_engine.llm_engine.model_executor.driver_worker.worker.model_runner.model
+ model.load_weights(self._make_weight_iterator(actor_weights))
+ print_gpu_memory_usage("After sync model weights in sharding manager")
+
+ del actor_weights
+ torch.cuda.empty_cache()
+
+ if "tags" in inspect.signature(self.inference_engine.wake_up).parameters:
+ self.inference_engine.wake_up(tags=["kv_cache"])
+
+ print_gpu_memory_usage("After del state_dict and empty_cache in sharding manager")
+ # important: need to manually set the random states of each tp to be identical.
+ if self.device_mesh is not None:
+ self.torch_random_states = torch.cuda.get_rng_state()
+ torch.cuda.set_rng_state(self.gen_random_states)
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ print_gpu_memory_usage("Before vllm offload in sharding manager")
+ free_bytes_before_sleep = torch.cuda.mem_get_info()[0]
+ self.inference_engine.sleep(level=1)
+ free_bytes_after_sleep = torch.cuda.mem_get_info()[0]
+ self.freed_bytes = free_bytes_after_sleep - free_bytes_before_sleep
+ print_gpu_memory_usage("After vllm offload in sharding manager")
+
+ self.module.train()
+ torch.cuda.empty_cache() # add empty cache after each compute
+
+ # restore random states
+ if self.device_mesh is not None:
+ self.gen_random_states = torch.cuda.get_rng_state()
+ torch.cuda.set_rng_state(self.torch_random_states)
+
+ def preprocess_data(self, data: DataProto) -> DataProto:
+ """All gather across tp group to make each rank has identical input."""
+ all_gather_data_proto(data, size=self.tp_size, group=self.tp_group)
+ return data
+
+ def postprocess_data(self, data: DataProto) -> DataProto:
+ """Get chunk data of this tp rank since we do all gather in preprocess."""
+ if self.tp_size > 1:
+ data = data.chunk(chunks=self.tp_size)[self.tp_rank]
+
+ return data
diff --git a/LLaVA-NeXT/llava/__init__.py b/LLaVA-NeXT/llava/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d1f016db1028101d45ba7d68cb3f0bcb558c2bb
--- /dev/null
+++ b/LLaVA-NeXT/llava/__init__.py
@@ -0,0 +1 @@
+from .model import LlavaLlamaForCausalLM
diff --git a/LLaVA-NeXT/llava/constants.py b/LLaVA-NeXT/llava/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..be8cf0204969a6c973f442b383d8e425d684e826
--- /dev/null
+++ b/LLaVA-NeXT/llava/constants.py
@@ -0,0 +1,12 @@
+CONTROLLER_HEART_BEAT_EXPIRATION = 30
+WORKER_HEART_BEAT_INTERVAL = 15
+
+LOGDIR = "."
+
+# Model Constants
+IGNORE_INDEX = -100
+IMAGE_TOKEN_INDEX = -200
+DEFAULT_IMAGE_TOKEN = ""
+DEFAULT_IMAGE_PATCH_TOKEN = ""
+DEFAULT_IM_START_TOKEN = ""
+DEFAULT_IM_END_TOKEN = ""
diff --git a/LLaVA-NeXT/llava/conversation.py b/LLaVA-NeXT/llava/conversation.py
new file mode 100644
index 0000000000000000000000000000000000000000..66ef6963253d30e314a78372869091bd0181021a
--- /dev/null
+++ b/LLaVA-NeXT/llava/conversation.py
@@ -0,0 +1,597 @@
+import dataclasses
+from enum import auto, Enum
+from typing import List, Any, Dict, Union, Tuple
+import re
+import base64
+from io import BytesIO
+from PIL import Image
+from transformers import AutoTokenizer
+
+
+class SeparatorStyle(Enum):
+ """Different separator style."""
+
+ SINGLE = auto()
+ TWO = auto()
+ MPT = auto()
+ PLAIN = auto()
+ CHATML = auto()
+ LLAMA_2 = auto()
+ LLAMA_3 = auto()
+ QWEN = auto()
+ GEMMA = auto()
+
+
+@dataclasses.dataclass
+class Conversation:
+ """A class that keeps all conversation history."""
+
+ system: str
+ roles: List[str]
+ messages: List[List[str]]
+ offset: int
+ sep_style: SeparatorStyle = SeparatorStyle.SINGLE
+ sep: str = "###"
+ sep2: str = None
+ version: str = "Unknown"
+
+ tokenizer_id: str = ""
+ tokenizer: Any = None
+ # Stop criteria (the default one is EOS token)
+ stop_str: Union[str, List[str]] = None
+ # Stops generation if meeting any token in this list
+ stop_token_ids: List[int] = None
+
+ skip_next: bool = False
+
+ def get_prompt(self):
+ messages = self.messages
+ if len(messages) > 0 and type(messages[0][1]) is tuple:
+ messages = self.messages.copy()
+ init_role, init_msg = messages[0].copy()
+ init_msg = init_msg[0]
+ if "mmtag" in self.version:
+ init_msg = init_msg.replace("", "").strip()
+ messages[0] = (init_role, init_msg)
+ messages.insert(0, (self.roles[0], ""))
+ messages.insert(1, (self.roles[1], "Received."))
+ elif not init_msg.startswith(""):
+ init_msg = init_msg.replace("", "").strip()
+ messages[0] = (init_role, "\n" + init_msg)
+ else:
+ messages[0] = (init_role, init_msg)
+
+ if self.sep_style == SeparatorStyle.SINGLE:
+ ret = self.system + self.sep
+ for role, message in messages:
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ ret += role + ": " + message + self.sep
+ else:
+ ret += role + ":"
+
+ elif self.sep_style == SeparatorStyle.TWO:
+ seps = [self.sep, self.sep2]
+ ret = self.system + seps[0]
+ for i, (role, message) in enumerate(messages):
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ ret += role + ": " + message + seps[i % 2]
+ else:
+ ret += role + ":"
+
+ elif self.sep_style == SeparatorStyle.CHATML:
+ ret = "" if self.system == "" else self.system + self.sep + "\n"
+ for role, message in messages:
+ if message:
+ if type(message) is tuple:
+ message, images, _ = message
+ message = "" * len(images) + message
+ ret += role + "\n" + message + self.sep + "\n"
+ else:
+ ret += role + "\n"
+ return ret
+
+ elif self.sep_style == SeparatorStyle.LLAMA_3:
+ if self.tokenizer is None:
+ raise ValueError("Llama 3 tokenizer is not available. Make sure you have the necessary permissions.")
+ chat_template_messages = [{"role": "system", "content": self.system}]
+ for role, message in messages:
+ if message:
+ if type(message) is tuple:
+ message, images = message
+ message = "" * len(images) + message
+ chat_template_messages.append({"role": role, "content": message})
+
+ # print(chat_template_messages)
+ return self.tokenizer.apply_chat_template(chat_template_messages, tokenize=False, add_generation_prompt=True)
+ # ret = "" if self.system == "" else self.system + self.sep + "\n"
+ # for role, message in messages:
+ # if message:
+ # if type(message) is tuple:
+ # message, images = message
+ # message = "" * len(images) + message
+ # ret += role + "\n" + message + self.sep + "\n"
+ # else:
+ # ret += role + "\n"
+ # return ret
+
+ elif self.sep_style == SeparatorStyle.MPT:
+ ret = self.system + self.sep
+ for role, message in messages:
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ ret += role + message + self.sep
+ else:
+ ret += role
+
+ elif self.sep_style == SeparatorStyle.GEMMA:
+ ret = ""
+ for i, (role, message) in enumerate(messages):
+ assert role == self.roles[i % 2], "Conversation should alternate user/assistant/user/assistant/..."
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ ret += role + message + self.sep
+ else:
+ ret += role
+
+ elif self.sep_style == SeparatorStyle.LLAMA_2:
+ wrap_sys = lambda msg: f"<>\n{msg}\n<>\n\n" if len(msg) > 0 else msg
+ wrap_inst = lambda msg: f"[INST] {msg} [/INST]"
+ ret = ""
+
+ for i, (role, message) in enumerate(messages):
+ if i == 0:
+ assert message, "first message should not be none"
+ assert role == self.roles[0], "first message should come from user"
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ if i == 0:
+ message = wrap_sys(self.system) + message
+ if i % 2 == 0:
+ message = wrap_inst(message)
+ ret += self.sep + message
+ else:
+ ret += " " + message + " " + self.sep2
+ else:
+ ret += ""
+ ret = ret.lstrip(self.sep)
+
+ elif self.sep_style == SeparatorStyle.PLAIN:
+ seps = [self.sep, self.sep2]
+ ret = self.system
+ for i, (role, message) in enumerate(messages):
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ ret += message + seps[i % 2]
+ else:
+ ret += ""
+ else:
+ raise ValueError(f"Invalid style: {self.sep_style}")
+
+ return ret
+
+ def append_message(self, role, message):
+ self.messages.append([role, message])
+
+ def process_image(self, image, image_process_mode, return_pil=False, image_format="PNG"):
+ if image_process_mode == "Pad":
+
+ def expand2square(pil_img, background_color=(122, 116, 104)):
+ width, height = pil_img.size
+ if width == height:
+ return pil_img
+ elif width > height:
+ result = Image.new(pil_img.mode, (width, width), background_color)
+ result.paste(pil_img, (0, (width - height) // 2))
+ return result
+ else:
+ result = Image.new(pil_img.mode, (height, height), background_color)
+ result.paste(pil_img, ((height - width) // 2, 0))
+ return result
+
+ image = expand2square(image)
+ elif image_process_mode in ["Default", "Crop"]:
+ pass
+ elif image_process_mode == "Resize":
+ image = image.resize((336, 336))
+ else:
+ raise ValueError(f"Invalid image_process_mode: {image_process_mode}")
+
+ if type(image) is not Image.Image:
+ image = Image.open(image).convert("RGB")
+
+ max_hw, min_hw = max(image.size), min(image.size)
+ aspect_ratio = max_hw / min_hw
+ max_len, min_len = 672, 448
+ shortest_edge = int(min(max_len / aspect_ratio, min_len, min_hw))
+ longest_edge = int(shortest_edge * aspect_ratio)
+ W, H = image.size
+ if H > W:
+ H, W = longest_edge, shortest_edge
+ else:
+ H, W = shortest_edge, longest_edge
+ image = image.resize((W, H))
+ if return_pil:
+ return image
+ else:
+ buffered = BytesIO()
+ image.save(buffered, format=image_format)
+ img_b64_str = base64.b64encode(buffered.getvalue()).decode()
+ return img_b64_str
+
+ def get_images(self, return_pil=False, return_path=False):
+ images = []
+ for i, (role, msg) in enumerate(self.messages[self.offset :]):
+ if i % 2 == 0:
+ if type(msg) is tuple:
+ msg, image, image_process_mode = msg
+ if type(image) != list:
+ image = [image]
+ for img in image:
+ if not return_path and self.is_image_file(img):
+ img = self.process_image(img, image_process_mode, return_pil=return_pil)
+ else:
+ images.append(img)
+ return images
+
+ def is_image_file(self, filename):
+ image_extensions = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp"]
+ return any(filename.lower().endswith(ext) for ext in image_extensions)
+
+ def is_video_file(self, filename):
+ video_extensions = [".mp4", ".mov", ".avi", ".mkv", ".wmv", ".flv", ".mpeg", ".mpg"]
+ return any(filename.lower().endswith(ext) for ext in video_extensions)
+
+ def to_gradio_chatbot(self):
+ ret = []
+ for i, (role, msg) in enumerate(self.messages[self.offset :]):
+ if i % 2 == 0:
+ if type(msg) is tuple:
+ msg, image, image_process_mode = msg
+ if type(image) != list:
+ image = [image]
+ if len(image) == 1:
+ msg = "\n" + msg.replace("", "").strip()
+ else:
+ msg = re.sub(r"()\n(?=)", r"\1 ", msg)
+
+ img_str_list = []
+ for img in image:
+ if self.is_image_file(img):
+ img_b64_str = self.process_image(img, "Default", return_pil=False, image_format="JPEG")
+ img_str = f'
'
+ img_str_list.append(img_str)
+ elif self.is_video_file(img):
+ ret.append(((img,), None))
+
+ msg = msg.strip()
+ img_place_holder = ""
+ for img_str in img_str_list:
+ img_place_holder += f"{img_str}\n\n"
+
+ if len(img_str_list) > 0:
+ msg = f"{img_place_holder}\n\n{msg}"
+
+ if len(msg) > 0:
+ ret.append([msg, None])
+ else:
+ ret.append([msg, None])
+ else:
+ ret[-1][-1] = msg
+ return ret
+
+ def copy(self):
+ return Conversation(system=self.system, roles=self.roles, messages=[[x, y] for x, y in self.messages], offset=self.offset, sep_style=self.sep_style, sep=self.sep, sep2=self.sep2, version=self.version)
+
+ def dict(self):
+ if len(self.get_images()) > 0:
+ return {
+ "system": self.system,
+ "roles": self.roles,
+ "messages": [[x, y[0] if type(y) is tuple else y] for x, y in self.messages],
+ "offset": self.offset,
+ "sep": self.sep,
+ "sep2": self.sep2,
+ }
+ return {
+ "system": self.system,
+ "roles": self.roles,
+ "messages": self.messages,
+ "offset": self.offset,
+ "sep": self.sep,
+ "sep2": self.sep2,
+ }
+
+
+conv_vicuna_v0 = Conversation(
+ system="A chat between a curious human and an artificial intelligence assistant. " "The assistant gives helpful, detailed, and polite answers to the human's questions.",
+ roles=("Human", "Assistant"),
+ messages=[
+ ["Human", "What are the key differences between renewable and non-renewable energy sources?"],
+ [
+ "Assistant",
+ "Renewable energy sources are those that can be replenished naturally in a relatively "
+ "short amount of time, such as solar, wind, hydro, geothermal, and biomass. "
+ "Non-renewable energy sources, on the other hand, are finite and will eventually be "
+ "depleted, such as coal, oil, and natural gas. Here are some key differences between "
+ "renewable and non-renewable energy sources:\n"
+ "1. Availability: Renewable energy sources are virtually inexhaustible, while non-renewable "
+ "energy sources are finite and will eventually run out.\n"
+ "2. Environmental impact: Renewable energy sources have a much lower environmental impact "
+ "than non-renewable sources, which can lead to air and water pollution, greenhouse gas emissions, "
+ "and other negative effects.\n"
+ "3. Cost: Renewable energy sources can be more expensive to initially set up, but they typically "
+ "have lower operational costs than non-renewable sources.\n"
+ "4. Reliability: Renewable energy sources are often more reliable and can be used in more remote "
+ "locations than non-renewable sources.\n"
+ "5. Flexibility: Renewable energy sources are often more flexible and can be adapted to different "
+ "situations and needs, while non-renewable sources are more rigid and inflexible.\n"
+ "6. Sustainability: Renewable energy sources are more sustainable over the long term, while "
+ "non-renewable sources are not, and their depletion can lead to economic and social instability.\n",
+ ],
+ ],
+ offset=2,
+ sep_style=SeparatorStyle.SINGLE,
+ sep="###",
+)
+
+conv_vicuna_v1 = Conversation(
+ system="A chat between a curious user and an artificial intelligence assistant. " "The assistant gives helpful, detailed, and polite answers to the user's questions.",
+ roles=("USER", "ASSISTANT"),
+ version="v1",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.TWO,
+ sep=" ",
+ sep2="",
+)
+
+conv_llama_2 = Conversation(
+ system="""You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.
+
+If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.""",
+ roles=("USER", "ASSISTANT"),
+ version="llama_v2",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.LLAMA_2,
+ sep="",
+ sep2="",
+)
+
+conv_llava_llama_2 = Conversation(
+ system="You are a helpful language and vision assistant. " "You are able to understand the visual content that the user provides, " "and assist the user with a variety of tasks using natural language.",
+ roles=("USER", "ASSISTANT"),
+ version="llama_v2",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.LLAMA_2,
+ sep="",
+ sep2="",
+)
+
+def safe_load_tokenizer(tokenizer_id):
+ try:
+ return AutoTokenizer.from_pretrained(tokenizer_id)
+ except Exception:
+ return None
+
+conv_llava_llama_3 = Conversation(
+ system="You are a helpful language and vision assistant. " "You are able to understand the visual content that the user provides, " "and assist the user with a variety of tasks using natural language.",
+ roles=("user", "assistant"),
+ version="llama_v3",
+ messages=[],
+ offset=0,
+ sep="<|eot_id|>",
+ sep_style=SeparatorStyle.LLAMA_3,
+ tokenizer_id="meta-llama/Meta-Llama-3-8B-Instruct",
+ tokenizer=safe_load_tokenizer("meta-llama/Meta-Llama-3-8B-Instruct"),
+ stop_token_ids=[128009],
+)
+
+conv_mistral_instruct = Conversation(
+ system="",
+ roles=("USER", "ASSISTANT"),
+ version="llama_v2",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.LLAMA_2,
+ sep="",
+ sep2="",
+)
+
+conv_llava_llama_2_simple = Conversation(
+ system="Answer the questions about the visual content that the user provides.",
+ roles=("USER", "ASSISTANT"),
+ version="llama_v2",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.LLAMA_2,
+ sep="",
+ sep2="",
+)
+
+conv_llava_llama_2_mmtag = Conversation(
+ system="Answer the questions about the visual content that the user provides." "The visual content will be provided with the following format: visual content.",
+ roles=("USER", "ASSISTANT"),
+ version="llama_v2_mmtag",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.LLAMA_2,
+ sep="",
+ sep2="",
+)
+
+conv_mpt = Conversation(
+ system="""<|im_start|>system
+A conversation between a user and an LLM-based AI assistant. The assistant gives helpful and honest answers.""",
+ roles=("<|im_start|>user\n", "<|im_start|>assistant\n"),
+ version="mpt",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.MPT,
+ sep="<|im_end|>",
+)
+
+conv_qwen = Conversation(
+ system="""<|im_start|>system
+You are a helpful assistant.""",
+ roles=("<|im_start|>user", "<|im_start|>assistant"),
+ version="qwen",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.CHATML,
+ sep="<|im_end|>",
+)
+
+conv_qwen_2_5 = Conversation(
+ system="""<|im_start|>system
+You are Qwen, created by Alibaba Cloud. You are a helpful assistant.""",
+ roles=("<|im_start|>user", "<|im_start|>assistant"),
+ version="qwen",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.CHATML,
+ sep="<|im_end|>",
+)
+
+conv_gemma_instruct = Conversation(system="", roles=("user\n", "model\n"), version="gemma", messages=[], offset=0, sep_style=SeparatorStyle.GEMMA, sep="\n")
+
+conv_llava_plain = Conversation(
+ system="",
+ roles=("", ""),
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.PLAIN,
+ sep="\n",
+)
+
+conv_llava_v0 = Conversation(
+ system="A chat between a curious human and an artificial intelligence assistant. " "The assistant gives helpful, detailed, and polite answers to the human's questions.",
+ roles=("Human", "Assistant"),
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.SINGLE,
+ sep="###",
+)
+
+conv_llava_v0_mmtag = Conversation(
+ system="A chat between a curious user and an artificial intelligence assistant. "
+ "The assistant is able to understand the visual content that the user provides, and assist the user with a variety of tasks using natural language."
+ "The visual content will be provided with the following format: visual content.",
+ roles=("Human", "Assistant"),
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.SINGLE,
+ sep="###",
+ version="v0_mmtag",
+)
+
+conv_llava_v1 = Conversation(
+ system="A chat between a curious human and an artificial intelligence assistant. " "The assistant gives helpful, detailed, and polite answers to the human's questions.",
+ roles=("USER", "ASSISTANT"),
+ version="v1",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.TWO,
+ sep=" ",
+ sep2="",
+)
+
+conv_llava_v1_mmtag = Conversation(
+ system="A chat between a curious user and an artificial intelligence assistant. "
+ "The assistant is able to understand the visual content that the user provides, and assist the user with a variety of tasks using natural language."
+ "The visual content will be provided with the following format: visual content.",
+ roles=("USER", "ASSISTANT"),
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.TWO,
+ sep=" ",
+ sep2="",
+ version="v1_mmtag",
+)
+
+conv_mistral_orca = Conversation(
+ system="""<|im_start|>system
+You are MistralOrca, a large language model trained by Alignment Lab AI. Write out your reasoning step-by-step to be sure you get the right answers!""",
+ roles=("<|im_start|>user\n", "<|im_start|>assistant\n"),
+ version="mpt",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.MPT,
+ sep="<|im_end|>",
+)
+
+conv_mistral_zephyr = Conversation(
+ system="""<|system|>
+You are a helpful AI assistant.""",
+ roles=("<|user|>\n", "<|assistant|>\n"),
+ version="mpt",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.MPT,
+ sep="",
+)
+
+conv_mistral_direct = Conversation(
+ system="""<|im_start|>system
+Answer the questions.""",
+ roles=("<|im_start|>user\n", "<|im_start|>assistant\n"),
+ version="mpt",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.MPT,
+ sep="<|im_end|>",
+)
+
+conv_chatml_direct = Conversation(
+ system="""<|im_start|>system
+Answer the questions.""",
+ roles=("<|im_start|>user\n", "<|im_start|>assistant\n"),
+ version="mpt",
+ messages=[],
+ offset=0,
+ sep_style=SeparatorStyle.MPT,
+ sep="<|im_end|>",
+)
+
+default_conversation = conv_vicuna_v0
+conv_templates = {
+ "default": conv_vicuna_v0,
+ "v0": conv_vicuna_v0,
+ "v1": conv_vicuna_v1,
+ "vicuna_v1": conv_vicuna_v1,
+ "llama_2": conv_llama_2,
+ "mistral_instruct": conv_mistral_instruct,
+ "mistral_orca": conv_mistral_orca,
+ "mistral_zephyr": conv_mistral_zephyr,
+ "mistral_direct": conv_mistral_direct,
+ "plain": conv_llava_plain,
+ "v0_plain": conv_llava_plain,
+ "chatml_direct": conv_chatml_direct,
+ "llava_v0": conv_llava_v0,
+ "llava_v0_mmtag": conv_llava_v0_mmtag,
+ "llava_v1": conv_llava_v1,
+ "llava_v1_mmtag": conv_llava_v1_mmtag,
+ "llava_llama_2": conv_llava_llama_2,
+ "llava_llama_3": conv_llava_llama_3,
+ "llava_llama_2_simple": conv_llava_llama_2_simple,
+ "llava_llama_2_mmtag": conv_llava_llama_2_mmtag,
+ "llava_mistral_instruct": conv_mistral_instruct,
+ "mpt": conv_mpt,
+ "qwen_1_5": conv_qwen,
+ "qwen_2": conv_qwen,
+ "qwen_2_5": conv_qwen_2_5,
+ "gemma_instruct": conv_gemma_instruct,
+}
+
+
+if __name__ == "__main__":
+ print(default_conversation.get_prompt())
diff --git a/LLaVA-NeXT/llava/mm_utils.py b/LLaVA-NeXT/llava/mm_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..62a3e50905a9c18799ade86af7d62c59eb029115
--- /dev/null
+++ b/LLaVA-NeXT/llava/mm_utils.py
@@ -0,0 +1,395 @@
+from PIL import Image
+from io import BytesIO
+import base64
+import math
+import ast
+import re
+import torch
+from transformers import StoppingCriteria
+from llava.constants import IMAGE_TOKEN_INDEX
+
+
+def resize_and_center_crop(image, shortest_edge_length):
+ # Calculate new dimensions and resize
+ aspect_ratio = float(image.width) / float(image.height)
+ if aspect_ratio > 1:
+ new_width = int(shortest_edge_length * aspect_ratio)
+ new_height = shortest_edge_length
+ else:
+ new_width = shortest_edge_length
+ new_height = int(shortest_edge_length / aspect_ratio)
+ resized_image = image.resize((new_width, new_height), Image.ANTIALIAS)
+
+ # Calculate the position and perform the center crop
+ left = (new_width - shortest_edge_length) / 2
+ top = (new_height - shortest_edge_length) / 2
+ right = (new_width + shortest_edge_length) / 2
+ bottom = (new_height + shortest_edge_length) / 2
+ cropped_image = resized_image.crop((left, top, right, bottom))
+
+ return cropped_image
+
+
+def auto_pad_images(image, grid_params):
+ assert isinstance(image, Image.Image), "Input should be a Pillow Image"
+ assert len(grid_params) > 0, "Grid parameters should not be empty"
+
+ # Step 1: Calculate and find the closest aspect ratio
+ input_width, input_height = image.size
+ input_aspect_ratio = input_width / input_height
+ candidate_resolutions = [(w / h, w, h) for w in grid_params for h in grid_params]
+ closest_aspect_ratio = min(candidate_resolutions, key=lambda x: abs(input_aspect_ratio - x[0]))
+
+ candidate_resolutions = [(x[1], x[2]) for x in candidate_resolutions if abs(x[0] - closest_aspect_ratio[0]) < 1e-3]
+
+ target_resolution = min(candidate_resolutions, key=lambda res: abs(max(input_width, input_height) / max(res) - 1))
+
+ resize_width, resize_height = target_resolution
+ if input_width > input_height:
+ resize_height = int(resize_width / input_aspect_ratio)
+ else:
+ resize_width = int(resize_height * input_aspect_ratio)
+ resized_image = image.resize((resize_width, resize_height), Image.ANTIALIAS)
+
+ # Step 5: Pad the resized image if necessary to match the target resolution
+ pad_width = target_resolution[0] - resize_width
+ pad_height = target_resolution[1] - resize_height
+ padded_image = Image.new("RGB", target_resolution, color=(0, 0, 0))
+ padded_image.paste(resized_image, (pad_width // 2, pad_height // 2))
+
+ return padded_image
+
+
+def extract_patches(image, patch_size, overlap_ratio):
+ assert isinstance(image, Image.Image), "Input should be a Pillow Image"
+ assert patch_size > 0, "Patch size should be greater than 0"
+ assert 0 <= overlap_ratio < 1, "Overlap ratio should be between 0 and 1"
+
+ W, H = image.size
+ patches = []
+
+ stride = int(patch_size * (1 - overlap_ratio))
+
+ num_patches_y = (H - patch_size) // stride + 1
+ num_patches_x = (W - patch_size) // stride + 1
+
+ y_start = (H - (num_patches_y - 1) * stride - patch_size) // 2
+ x_start = (W - (num_patches_x - 1) * stride - patch_size) // 2
+
+ for y in range(y_start, y_start + num_patches_y * stride, stride):
+ for x in range(x_start, x_start + num_patches_x * stride, stride):
+ patch = image.crop((x, y, x + patch_size, y + patch_size))
+ patches.append(patch)
+
+ return patches
+
+
+def process_highres_image_crop_split(image, data_args, processor=None):
+ crop_resolution = data_args.image_crop_resolution
+ split_resolution = data_args.image_split_resolution
+ if processor is None:
+ processor = data_args.image_processor
+ image_crop = resize_and_center_crop(image, crop_resolution)
+ image_patches = extract_patches(image_crop, patch_size=split_resolution, overlap_ratio=0)
+ image_patches = [processor.preprocess(image_patch, return_tensors="pt")["pixel_values"][0] for image_patch in image_patches]
+ return torch.stack(image_patches, dim=0)
+
+
+def process_highres_image(image, processor, grid_pinpoints):
+ grid_params = [int(x) for x in grid_pinpoints.split(",")]
+ width_height = max(image.size)
+ fit_grid_params = [x for x in grid_params if x >= width_height]
+ if len(fit_grid_params) == 0:
+ select_size = max(grid_params)
+ else:
+ select_size = min(fit_grid_params)
+ # FIXME: always select the 448
+ select_size = max(grid_params)
+ image_padded = expand2square(image, tuple(int(x * 255) for x in processor.image_mean))
+
+ # FIXME: this seems to be a bug that it always resizes instead of padding
+ image_original_resize = image.resize((processor.size["shortest_edge"], processor.size["shortest_edge"]))
+ image_padded = image_padded.resize((select_size, select_size))
+ image_patches = extract_patches(image_padded, patch_size=processor.size["shortest_edge"], overlap_ratio=0)
+ image_patches = [image_original_resize] + image_patches
+ image_patches = [processor.preprocess(image_patch, return_tensors="pt")["pixel_values"][0] for image_patch in image_patches]
+ return torch.stack(image_patches, dim=0)
+
+
+def select_best_resolution(original_size, possible_resolutions):
+ """
+ Selects the best resolution from a list of possible resolutions based on the original size.
+
+ Args:
+ original_size (tuple): The original size of the image in the format (width, height).
+ possible_resolutions (list): A list of possible resolutions in the format [(width1, height1), (width2, height2), ...].
+
+ Returns:
+ tuple: The best fit resolution in the format (width, height).
+ """
+ original_width, original_height = original_size
+ best_fit = None
+ max_effective_resolution = 0
+ min_wasted_resolution = float("inf")
+
+ for width, height in possible_resolutions:
+ # Calculate the downscaled size to keep the aspect ratio
+ scale = min(width / original_width, height / original_height)
+ downscaled_width, downscaled_height = int(original_width * scale), int(original_height * scale)
+
+ # Calculate effective and wasted resolutions
+ effective_resolution = min(downscaled_width * downscaled_height, original_width * original_height)
+ wasted_resolution = (width * height) - effective_resolution
+
+ if effective_resolution > max_effective_resolution or (effective_resolution == max_effective_resolution and wasted_resolution < min_wasted_resolution):
+ max_effective_resolution = effective_resolution
+ min_wasted_resolution = wasted_resolution
+ best_fit = (width, height)
+
+ return best_fit
+
+
+def resize_and_pad_image(image, target_resolution):
+ """
+ Resize and pad an image to a target resolution while maintaining aspect ratio.
+
+ Args:
+ image (PIL.Image.Image): The input image.
+ target_resolution (tuple): The target resolution (width, height) of the image.
+
+ Returns:
+ PIL.Image.Image: The resized and padded image.
+ """
+ original_width, original_height = image.size
+ target_width, target_height = target_resolution
+
+ # Determine which dimension (width or height) to fill
+ scale_w = target_width / original_width
+ scale_h = target_height / original_height
+
+ if scale_w < scale_h:
+ # Width will be filled completely
+ new_width = target_width
+ new_height = min(math.ceil(original_height * scale_w), target_height)
+ else:
+ # Height will be filled completely
+ new_height = target_height
+ new_width = min(math.ceil(original_width * scale_h), target_width)
+
+ # Resize the image
+ resized_image = image.resize((new_width, new_height))
+
+ # Create a new image with the target size and paste the resized image onto it
+ new_image = Image.new("RGB", (target_width, target_height), (0, 0, 0))
+ paste_x = (target_width - new_width) // 2
+ paste_y = (target_height - new_height) // 2
+ new_image.paste(resized_image, (paste_x, paste_y))
+
+ return new_image
+
+
+def divide_to_patches(image, patch_size):
+ """
+ Divides an image into patches of a specified size.
+
+ Args:
+ image (PIL.Image.Image): The input image.
+ patch_size (int): The size of each patch.
+
+ Returns:
+ list: A list of PIL.Image.Image objects representing the patches.
+ """
+ patches = []
+ width, height = image.size
+ for i in range(0, height, patch_size):
+ for j in range(0, width, patch_size):
+ box = (j, i, j + patch_size, i + patch_size)
+ patch = image.crop(box)
+ patches.append(patch)
+
+ return patches
+
+
+def get_anyres_image_grid_shape(image_size, grid_pinpoints, patch_size):
+ """
+ Calculate the shape of the image patch grid after the preprocessing for images of any resolution.
+
+ Args:
+ image_size (tuple): The size of the input image in the format (width, height).
+ grid_pinpoints (str): A string representation of a list of possible resolutions.
+ patch_size (int): The size of each image patch.
+
+ Returns:
+ tuple: The shape of the image patch grid in the format (width, height).
+ """
+ if isinstance(grid_pinpoints, str) and "x" in grid_pinpoints:
+ assert patch_size in [224, 336, 384, 448, 512], "patch_size should be in [224, 336, 384, 448, 512]"
+ # Use regex to extract the range from the input string
+ matches = re.findall(r"\((\d+)x(\d+)\)", grid_pinpoints)
+ range_start = tuple(map(int, matches[0]))
+ range_end = tuple(map(int, matches[-1]))
+ # Generate a matrix of tuples from (range_start[0], range_start[1]) to (range_end[0], range_end[1])
+ grid_pinpoints = [(i, j) for i in range(range_start[0], range_end[0] + 1) for j in range(range_start[1], range_end[1] + 1)]
+ # Multiply all elements by patch_size
+ grid_pinpoints = [[dim * patch_size for dim in pair] for pair in grid_pinpoints]
+ if type(grid_pinpoints) is list:
+ possible_resolutions = grid_pinpoints
+ else:
+ possible_resolutions = ast.literal_eval(grid_pinpoints)
+ width, height = select_best_resolution(image_size, possible_resolutions)
+ return width // patch_size, height // patch_size
+
+
+def process_anyres_image(image, processor, grid_pinpoints):
+ """
+ Process an image with variable resolutions.
+
+ Args:
+ image (PIL.Image.Image): The input image to be processed.
+ processor: The image processor object.
+ grid_pinpoints (str): A string representation of a list of possible resolutions.
+
+ Returns:
+ torch.Tensor: A tensor containing the processed image patches.
+ """
+ # Convert grid_pinpoints from string to list
+ if isinstance(grid_pinpoints, str) and "x" in grid_pinpoints:
+ try:
+ patch_size = processor.size[0]
+ except Exception as e:
+ patch_size = processor.size["shortest_edge"]
+ assert patch_size in [224, 336, 384, 448, 512], "patch_size should be in [224, 336, 384, 448, 512]"
+ # Use regex to extract the range from the input string
+ matches = re.findall(r"\((\d+)x(\d+)\)", grid_pinpoints)
+ range_start = tuple(map(int, matches[0]))
+ range_end = tuple(map(int, matches[-1]))
+ # Generate a matrix of tuples from (range_start[0], range_start[1]) to (range_end[0], range_end[1])
+ grid_pinpoints = [(i, j) for i in range(range_start[0], range_end[0] + 1) for j in range(range_start[1], range_end[1] + 1)]
+ # Multiply all elements by patch_size
+ grid_pinpoints = [[dim * patch_size for dim in pair] for pair in grid_pinpoints]
+
+ if type(grid_pinpoints) is list:
+ possible_resolutions = grid_pinpoints
+ else:
+ possible_resolutions = ast.literal_eval(grid_pinpoints)
+ best_resolution = select_best_resolution(image.size, possible_resolutions)
+ image_padded = resize_and_pad_image(image, best_resolution)
+
+ patches = divide_to_patches(image_padded, processor.crop_size["height"])
+
+ # FIXME: this seems to be a bug that it resizes instead of pad.
+ # but to keep it consistent with previous, i will keep it as it is
+ # TODO: uncomment below to ablate with the padding
+ if isinstance(processor.size, dict):
+ shortest_edge = processor.size["shortest_edge"]
+ else:
+ shortest_edge = min(processor.size)
+ image_original_resize = image.resize((shortest_edge, shortest_edge))
+ # image_padded_square = expand2square(image, tuple(int(x*255) for x in processor.image_mean))
+ # image_original_resize = image_padded_square.resize((processor.size['shortest_edge'], processor.size['shortest_edge']))
+
+ image_patches = [image_original_resize] + patches
+ image_patches = [processor.preprocess(image_patch, return_tensors="pt")["pixel_values"][0] for image_patch in image_patches]
+ return torch.stack(image_patches, dim=0)
+
+
+def load_image_from_base64(image):
+ return Image.open(BytesIO(base64.b64decode(image)))
+
+
+def expand2square(pil_img, background_color):
+ width, height = pil_img.size
+ if width == height:
+ return pil_img
+ elif width > height:
+ result = Image.new(pil_img.mode, (width, width), background_color)
+ result.paste(pil_img, (0, (width - height) // 2))
+ return result
+ else:
+ result = Image.new(pil_img.mode, (height, height), background_color)
+ result.paste(pil_img, ((height - width) // 2, 0))
+ return result
+
+
+def process_images(images, image_processor, model_cfg):
+ image_aspect_ratio = getattr(model_cfg, "image_aspect_ratio", None)
+ new_images = []
+ if image_aspect_ratio == "highres":
+ for image in images:
+ image = process_highres_image(image, image_processor, model_cfg.image_grid_pinpoints)
+ new_images.append(image)
+ elif image_aspect_ratio == "anyres" or "anyres_max" in image_aspect_ratio:
+ for image in images:
+ image = process_anyres_image(image, image_processor, model_cfg.image_grid_pinpoints)
+ new_images.append(image)
+ elif image_aspect_ratio == "crop_split":
+ for image in images:
+ image = process_highres_image_crop_split(image, model_cfg, image_processor)
+ new_images.append(image)
+ elif image_aspect_ratio == "pad":
+ for image in images:
+ image = expand2square(image, tuple(int(x * 255) for x in image_processor.image_mean))
+ image = image_processor.preprocess(image, return_tensors="pt")["pixel_values"][0]
+ new_images.append(image)
+ else:
+ return image_processor.preprocess(images, return_tensors="pt")["pixel_values"]
+ if all(x.shape == new_images[0].shape for x in new_images):
+ new_images = torch.stack(new_images, dim=0)
+ return new_images
+
+
+def tokenizer_image_token(prompt, tokenizer, image_token_index=IMAGE_TOKEN_INDEX, return_tensors=None):
+ prompt_chunks = [tokenizer(chunk).input_ids for chunk in prompt.split("")]
+
+ def insert_separator(X, sep):
+ return [ele for sublist in zip(X, [sep] * len(X)) for ele in sublist][:-1]
+
+ input_ids = []
+ offset = 0
+ if len(prompt_chunks) > 0 and len(prompt_chunks[0]) > 0 and prompt_chunks[0][0] == tokenizer.bos_token_id:
+ offset = 1
+ input_ids.append(prompt_chunks[0][0])
+
+ for x in insert_separator(prompt_chunks, [image_token_index] * (offset + 1)):
+ input_ids.extend(x[offset:])
+
+ if return_tensors is not None:
+ if return_tensors == "pt":
+ return torch.tensor(input_ids, dtype=torch.long)
+ raise ValueError(f"Unsupported tensor type: {return_tensors}")
+ return input_ids
+
+
+def get_model_name_from_path(model_path):
+ model_path = model_path.strip("/")
+ model_paths = model_path.split("/")
+ if model_paths[-1].startswith("checkpoint-"):
+ return model_paths[-2] + "_" + model_paths[-1]
+ else:
+ return model_paths[-1]
+
+
+class KeywordsStoppingCriteria(StoppingCriteria):
+ def __init__(self, keywords, tokenizer, input_ids):
+ self.keywords = keywords
+ self.keyword_ids = []
+ for keyword in keywords:
+ cur_keyword_ids = tokenizer(keyword).input_ids
+ if len(cur_keyword_ids) > 1 and cur_keyword_ids[0] == tokenizer.bos_token_id:
+ cur_keyword_ids = cur_keyword_ids[1:]
+ self.keyword_ids.append(torch.tensor(cur_keyword_ids))
+ self.tokenizer = tokenizer
+ self.start_len = input_ids.shape[1]
+
+ def __call__(self, output_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:
+ assert output_ids.shape[0] == 1, "Only support batch size 1 (yet)" # TODO
+ offset = min(output_ids.shape[1] - self.start_len, 3)
+ self.keyword_ids = [keyword_id.to(output_ids.device) for keyword_id in self.keyword_ids]
+ for keyword_id in self.keyword_ids:
+ if output_ids[0, -keyword_id.shape[0] :] == keyword_id:
+ return True
+ outputs = self.tokenizer.batch_decode(output_ids[:, -offset:], skip_special_tokens=True)[0]
+ for keyword in self.keywords:
+ if keyword in outputs:
+ return True
+ return False
diff --git a/LLaVA-NeXT/llava/model/__init__.py b/LLaVA-NeXT/llava/model/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1abf0b3629098c96a54fb6942e87e9f44e0e217d
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/__init__.py
@@ -0,0 +1,16 @@
+import os
+
+AVAILABLE_MODELS = {
+ "llava_llama": "LlavaLlamaForCausalLM, LlavaConfig",
+ "llava_qwen": "LlavaQwenForCausalLM, LlavaQwenConfig",
+ "llava_mistral": "LlavaMistralForCausalLM, LlavaMistralConfig",
+ "llava_mixtral": "LlavaMixtralForCausalLM, LlavaMixtralConfig",
+ # "llava_qwen_moe": "LlavaQwenMoeForCausalLM, LlavaQwenMoeConfig",
+ # Add other models as needed
+}
+
+for model_name, model_classes in AVAILABLE_MODELS.items():
+ try:
+ exec(f"from .language_model.{model_name} import {model_classes}")
+ except Exception as e:
+ print(f"Failed to import {model_name} from llava.language_model.{model_name}. Error: {e}")
diff --git a/LLaVA-NeXT/llava/model/apply_delta.py b/LLaVA-NeXT/llava/model/apply_delta.py
new file mode 100644
index 0000000000000000000000000000000000000000..c183ba19a4e91e9cb95155b542e7406ea5b287a0
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/apply_delta.py
@@ -0,0 +1,47 @@
+"""
+Usage:
+python3 -m fastchat.model.apply_delta --base ~/model_weights/llama-7b --target ~/model_weights/vicuna-7b --delta lmsys/vicuna-7b-delta
+"""
+
+import argparse
+
+import torch
+from tqdm import tqdm
+from transformers import AutoTokenizer, AutoModelForCausalLM
+from llava import LlavaLlamaForCausalLM
+
+
+def apply_delta(base_model_path, target_model_path, delta_path):
+ print("Loading base model")
+ base = AutoModelForCausalLM.from_pretrained(base_model_path, torch_dtype=torch.float16, low_cpu_mem_usage=True)
+
+ print("Loading delta")
+ delta = LlavaLlamaForCausalLM.from_pretrained(delta_path, torch_dtype=torch.float16, low_cpu_mem_usage=True)
+ delta_tokenizer = AutoTokenizer.from_pretrained(delta_path)
+
+ print("Applying delta")
+ for name, param in tqdm(delta.state_dict().items(), desc="Applying delta"):
+ if name not in base.state_dict():
+ assert name in ["model.mm_projector.weight", "model.mm_projector.bias"], f"{name} not in base model"
+ continue
+ if param.data.shape == base.state_dict()[name].shape:
+ param.data += base.state_dict()[name]
+ else:
+ assert name in ["model.embed_tokens.weight", "lm_head.weight"], f"{name} dimension mismatch: {param.data.shape} vs {base.state_dict()[name].shape}"
+ bparam = base.state_dict()[name]
+ param.data[: bparam.shape[0], : bparam.shape[1]] += bparam
+
+ print("Saving target model")
+ delta.save_pretrained(target_model_path)
+ delta_tokenizer.save_pretrained(target_model_path)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--base-model-path", type=str, required=True)
+ parser.add_argument("--target-model-path", type=str, required=True)
+ parser.add_argument("--delta-path", type=str, required=True)
+
+ args = parser.parse_args()
+
+ apply_delta(args.base_model_path, args.target_model_path, args.delta_path)
diff --git a/LLaVA-NeXT/llava/model/builder.py b/LLaVA-NeXT/llava/model/builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9e8ee019e8dddc8394100465c26fb930f09884a
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/builder.py
@@ -0,0 +1,318 @@
+# Copyright 2023 Haotian Liu
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import warnings
+import shutil
+
+from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig, BitsAndBytesConfig
+import torch
+from llava.model import *
+from llava.constants import DEFAULT_IMAGE_PATCH_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
+from llava.utils import rank0_print
+
+
+def _align_config_vocab_with_tokenizer(cfg, tokenizer):
+ if cfg is None or tokenizer is None:
+ return
+ vocab_size = len(tokenizer)
+ if hasattr(cfg, "vocab_size"):
+ cfg.vocab_size = vocab_size
+ text_cfg = getattr(cfg, "text_config", None)
+ if text_cfg is not None and hasattr(text_cfg, "vocab_size"):
+ text_cfg.vocab_size = vocab_size
+
+
+def load_pretrained_model(model_path, model_base, model_name, load_8bit=False, load_4bit=False, device_map="auto", torch_dtype="float16",attn_implementation="flash_attention_2", customized_config=None, overwrite_config=None, **kwargs):
+ kwargs["device_map"] = device_map
+
+ if load_8bit:
+ kwargs["load_in_8bit"] = True
+ elif load_4bit:
+ kwargs["load_in_4bit"] = True
+ kwargs["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4")
+ elif torch_dtype == "float16":
+ kwargs["torch_dtype"] = torch.float16
+ elif torch_dtype == "bfloat16":
+ kwargs["torch_dtype"] = torch.bfloat16
+ else:
+ import pdb;pdb.set_trace()
+
+ if customized_config is not None:
+ kwargs["config"] = customized_config
+
+ if "multimodal" in kwargs:
+ if kwargs["multimodal"] is True:
+ is_multimodal = True
+ kwargs.pop("multimodal")
+ else:
+ is_multimodal = False
+
+ if "llava" in model_name.lower() or is_multimodal:
+ # Load LLaVA model
+ if "lora" in model_name.lower() and model_base is None:
+ warnings.warn(
+ "There is `lora` in model name but no `model_base` is provided. If you are loading a LoRA model, please provide the `model_base` argument. Detailed instruction: https://github.com/haotian-liu/LLaVA#launch-a-model-worker-lora-weights-unmerged."
+ )
+ if "lora" in model_name.lower() and model_base is not None:
+ lora_cfg_pretrained = AutoConfig.from_pretrained(model_path)
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ rank0_print("Loading LLaVA from base model...")
+ if "mixtral" in model_name.lower():
+ from llava.model.language_model.llava_mixtral import LlavaMixtralConfig
+
+ lora_cfg_pretrained = LlavaMixtralConfig.from_pretrained(model_path)
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ model = LlavaMixtralForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=lora_cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ elif "mistral" in model_name.lower():
+ from llava.model.language_model.llava_mistral import LlavaMistralConfig
+
+ lora_cfg_pretrained = LlavaMistralConfig.from_pretrained(model_path)
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ model = LlavaMistralForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=lora_cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ elif "gemma" in model_name.lower():
+ from llava.model.language_model.llava_gemma import LlavaGemmaConfig
+
+ lora_cfg_pretrained = LlavaGemmaConfig.from_pretrained(model_path)
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ model = LlavaGemmaForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=lora_cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ else:
+ from llava.model.language_model.llava_llama import LlavaConfig
+
+ lora_cfg_pretrained = LlavaConfig.from_pretrained(model_path)
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ model = LlavaLlamaForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=lora_cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+
+ token_num, tokem_dim = model.lm_head.out_features, model.lm_head.in_features
+ if model.lm_head.weight.shape[0] != token_num:
+ model.lm_head.weight = torch.nn.Parameter(torch.empty(token_num, tokem_dim, device=model.device, dtype=model.dtype))
+ model.model.embed_tokens.weight = torch.nn.Parameter(torch.empty(token_num, tokem_dim, device=model.device, dtype=model.dtype))
+
+ rank0_print("Loading additional LLaVA weights...")
+ if os.path.exists(os.path.join(model_path, "non_lora_trainables.bin")):
+ non_lora_trainables = torch.load(os.path.join(model_path, "non_lora_trainables.bin"), map_location="cpu")
+ else:
+ # this is probably from HF Hub
+ from huggingface_hub import hf_hub_download
+
+ def load_from_hf(repo_id, filename, subfolder=None):
+ cache_file = hf_hub_download(repo_id=repo_id, filename=filename, subfolder=subfolder)
+ return torch.load(cache_file, map_location="cpu")
+
+ non_lora_trainables = load_from_hf(model_path, "non_lora_trainables.bin")
+ non_lora_trainables = {(k[11:] if k.startswith("base_model.") else k): v for k, v in non_lora_trainables.items()}
+ if any(k.startswith("model.model.") for k in non_lora_trainables):
+ non_lora_trainables = {(k[6:] if k.startswith("model.") else k): v for k, v in non_lora_trainables.items()}
+ model.load_state_dict(non_lora_trainables, strict=False)
+
+ from peft import PeftModel
+
+ rank0_print("Loading LoRA weights...")
+ model = PeftModel.from_pretrained(model, model_path)
+ rank0_print("Merging LoRA weights...")
+ model = model.merge_and_unload()
+ rank0_print("Model is loaded...")
+ elif model_base is not None: # this may be mm projector only, loading projector with preset language mdoel
+ rank0_print(f"Loading LLaVA from base model {model_base}...")
+ if "mixtral" in model_name.lower():
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ cfg_pretrained = AutoConfig.from_pretrained(model_path)
+ model = LlavaMixtralForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ elif "mistral" in model_name.lower() or "zephyr" in model_name.lower():
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ cfg_pretrained = AutoConfig.from_pretrained(model_path)
+ model = LlavaMistralForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ elif "gemma" in model_name.lower():
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ cfg_pretrained = AutoConfig.from_pretrained(model_path)
+ model = LlavaGemmaForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ elif (
+ "wizardlm-2" in model_name.lower()
+ and "vicuna" in model_name.lower()
+ or "llama" in model_name.lower()
+ or "yi" in model_name.lower()
+ or "nous-hermes" in model_name.lower()
+ or "llava-v1.6-34b" in model_name.lower()
+ or "llava-v1.5" in model_name.lower()
+ ):
+ from llava.model.language_model.llava_llama import LlavaConfig
+
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
+ if customized_config is None:
+ llava_cfg = LlavaConfig.from_pretrained(model_path)
+ if "v1.5" in model_name.lower():
+ llava_cfg.delay_load = True # a workaround for correctly loading v1.5 models
+ else:
+ llava_cfg = customized_config
+
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ llava_cfg = LlavaConfig.from_pretrained(model_path)
+ model = LlavaLlamaForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=llava_cfg, **kwargs)
+ else:
+ raise ValueError(f"Model {model_name} not supported")
+
+ mm_projector_weights = torch.load(os.path.join(model_path, "mm_projector.bin"), map_location="cpu")
+ mm_projector_weights = {k: v.to(torch.float16) for k, v in mm_projector_weights.items()}
+ model.load_state_dict(mm_projector_weights, strict=False)
+ else:
+ rank0_print(f"Loaded LLaVA model: {model_path}")
+ if "mixtral" in model_name.lower():
+ from llava.model.language_model.llava_mixtral import LlavaMixtralConfig
+
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
+ if customized_config is None:
+ llava_cfg = LlavaMixtralConfig.from_pretrained(model_path)
+ else:
+ llava_cfg = customized_config
+
+ if overwrite_config is not None:
+ rank0_print(f"Overwriting config with {overwrite_config}")
+ for k, v in overwrite_config.items():
+ setattr(llava_cfg, k, v)
+
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
+ model = LlavaMixtralForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, config=llava_cfg, **kwargs)
+
+ elif "mistral" in model_name.lower() or "zephyr" in model_name.lower():
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
+ model = LlavaMistralForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, **kwargs)
+ elif (
+ "wizardlm-2" in model_name.lower()
+ and "vicuna" in model_name.lower()
+ or "llama" in model_name.lower()
+ or "yi" in model_name.lower()
+ or "nous-hermes" in model_name.lower()
+ or "llava-v1.6-34b" in model_name.lower()
+ or "llava-v1.5" in model_name.lower()
+ ):
+ from llava.model.language_model.llava_llama import LlavaConfig
+
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
+ if customized_config is None:
+ llava_cfg = LlavaConfig.from_pretrained(model_path)
+ if "v1.5" in model_name.lower():
+ llava_cfg.delay_load = True # a workaround for correctly loading v1.5 models
+ else:
+ llava_cfg = customized_config
+
+ if overwrite_config is not None:
+ rank0_print(f"Overwriting config with {overwrite_config}")
+ for k, v in overwrite_config.items():
+ setattr(llava_cfg, k, v)
+ _align_config_vocab_with_tokenizer(llava_cfg, tokenizer)
+
+ model = LlavaLlamaForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, config=llava_cfg, **kwargs)
+
+ elif "qwen" in model_name.lower() or "quyen" in model_name.lower():
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
+ if "moe" in model_name.lower() or "A14B" in model_name.lower():
+ from llava.model.language_model.llava_qwen_moe import LlavaQwenMoeConfig
+ if overwrite_config is not None:
+ llava_cfg = LlavaQwenMoeConfig.from_pretrained(model_path)
+ rank0_print(f"Overwriting config with {overwrite_config}")
+ for k, v in overwrite_config.items():
+ setattr(llava_cfg, k, v)
+ model = LlavaQwenMoeForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, config=llava_cfg, **kwargs)
+ else:
+ model = LlavaQwenMoeForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, **kwargs)
+
+ else:
+ from llava.model.language_model.llava_qwen import LlavaQwenConfig
+ if overwrite_config is not None:
+ llava_cfg = LlavaQwenConfig.from_pretrained(model_path)
+ rank0_print(f"Overwriting config with {overwrite_config}")
+ for k, v in overwrite_config.items():
+ setattr(llava_cfg, k, v)
+ model = LlavaQwenForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, config=llava_cfg, **kwargs)
+ else:
+ model = LlavaQwenForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, **kwargs)
+
+ elif "gemma" in model_name.lower():
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
+ cfg_pretrained = AutoConfig.from_pretrained(model_path)
+ model = LlavaGemmaForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, config=cfg_pretrained, attn_implementation=attn_implementation, **kwargs)
+ else:
+ try:
+ from llava.model.language_model.llava_llama import LlavaConfig
+
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
+ if customized_config is None:
+ llava_cfg = LlavaConfig.from_pretrained(model_path)
+ if "v1.5" in model_path.lower():
+ llava_cfg.delay_load = True # a workaround for correctly loading v1.5 models
+ else:
+ llava_cfg = customized_config
+
+ if overwrite_config is not None:
+ rank0_print(f"Overwriting config with {overwrite_config}")
+ for k, v in overwrite_config.items():
+ setattr(llava_cfg, k, v)
+ _align_config_vocab_with_tokenizer(llava_cfg, tokenizer)
+ model = LlavaLlamaForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, attn_implementation=attn_implementation, config=llava_cfg, **kwargs)
+ except:
+ raise ValueError(f"Model {model_name} not supported")
+
+ else:
+ # Load language model
+ if model_base is not None:
+ # PEFT model
+ from peft import PeftModel
+
+ tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
+ model = AutoModelForCausalLM.from_pretrained(model_base, torch_dtype=torch.float16, low_cpu_mem_usage=True, device_map="auto")
+ print(f"Loading LoRA weights from {model_path}")
+ model = PeftModel.from_pretrained(model, model_path)
+ print(f"Merging weights")
+ model = model.merge_and_unload()
+ print("Convert to FP16...")
+ model.to(torch.float16)
+ else:
+ use_fast = False
+ if "mpt" in model_name.lower().replace("prompt", ""):
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=True)
+ model = AutoModelForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, trust_remote_code=True, **kwargs)
+ else:
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
+ model = AutoModelForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True, **kwargs)
+
+ rank0_print(f"Model Class: {model.__class__.__name__}")
+ image_processor = None
+
+ if "llava" in model_name.lower() or is_multimodal:
+ mm_use_im_start_end = getattr(model.config, "mm_use_im_start_end", False)
+ mm_use_im_patch_token = getattr(model.config, "mm_use_im_patch_token", True)
+ if mm_use_im_patch_token:
+ tokenizer.add_tokens([DEFAULT_IMAGE_PATCH_TOKEN], special_tokens=True)
+ if mm_use_im_start_end:
+ tokenizer.add_tokens([DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN], special_tokens=True)
+ model.resize_token_embeddings(len(tokenizer))
+
+ vision_tower = model.get_vision_tower()
+ if not vision_tower.is_loaded:
+ vision_tower.load_model(device_map=device_map)
+ if device_map != "auto":
+ vision_tower.to(device="cuda", dtype=torch.float16)
+ image_processor = vision_tower.image_processor
+
+ if hasattr(model.config, "max_sequence_length"):
+ context_len = model.config.max_sequence_length
+ elif hasattr(model.config, "max_position_embeddings"):
+ context_len = model.config.max_position_embeddings
+ elif hasattr(model.config, "tokenizer_model_max_length"):
+ context_len = model.config.tokenizer_model_max_length
+ else:
+ context_len = 2048
+
+ return tokenizer, model, image_processor, context_len
diff --git a/LLaVA-NeXT/llava/model/consolidate.py b/LLaVA-NeXT/llava/model/consolidate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f02e575f6b8e4388e1758776cadd62309147a1ad
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/consolidate.py
@@ -0,0 +1,30 @@
+"""
+Usage:
+python3 -m llava.model.consolidate --src ~/model_weights/llava-7b --dst ~/model_weights/llava-7b_consolidate
+"""
+
+import argparse
+
+import torch
+from transformers import AutoTokenizer, AutoModelForCausalLM
+from llava.model import *
+from llava.model.utils import auto_upgrade
+
+
+def consolidate_ckpt(src_path, dst_path):
+ print("Loading model")
+ auto_upgrade(src_path)
+ src_model = AutoModelForCausalLM.from_pretrained(src_path, torch_dtype=torch.float16, low_cpu_mem_usage=True)
+ src_tokenizer = AutoTokenizer.from_pretrained(src_path, use_fast=False)
+ src_model.save_pretrained(dst_path)
+ src_tokenizer.save_pretrained(dst_path)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--src", type=str, required=True)
+ parser.add_argument("--dst", type=str, required=True)
+
+ args = parser.parse_args()
+
+ consolidate_ckpt(args.src, args.dst)
diff --git a/LLaVA-NeXT/llava/model/llava_arch.py b/LLaVA-NeXT/llava/model/llava_arch.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea98ae8501b0f52d8a1b61323bfc03f6bc3c2541
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/llava_arch.py
@@ -0,0 +1,597 @@
+# Copyright 2023 Haotian Liu
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from abc import ABC, abstractmethod
+
+import math
+import re
+import time
+import torch
+import torch.nn as nn
+from .multimodal_encoder.builder import build_vision_tower
+from .multimodal_resampler.builder import build_vision_resampler
+from .multimodal_projector.builder import build_vision_projector
+
+from llava.constants import IGNORE_INDEX, IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_PATCH_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
+
+from llava.mm_utils import get_anyres_image_grid_shape
+from llava.utils import rank0_print, rank_print
+import random
+
+
+class LlavaMetaModel:
+
+ def __init__(self, config):
+ super(LlavaMetaModel, self).__init__(config)
+
+ if hasattr(config, "mm_vision_tower"):
+ delay_load = getattr(config, "delay_load", False)
+ self.vision_tower = build_vision_tower(config, delay_load=delay_load)
+ self.vision_resampler = build_vision_resampler(config, vision_tower=self.vision_tower)
+ self.mm_projector = build_vision_projector(config, vision_cfg=self.vision_tower.config)
+
+ if "unpad" in getattr(config, "mm_patch_merge_type", ""):
+ self.image_newline = nn.Parameter(torch.empty(config.hidden_size, dtype=self.dtype))
+
+ def get_vision_tower(self):
+ vision_tower = getattr(self, "vision_tower", None)
+ if type(vision_tower) is list:
+ vision_tower = vision_tower[0]
+ return vision_tower
+
+ def initialize_vision_modules(self, model_args, fsdp=None):
+ vision_tower = model_args.vision_tower
+ mm_vision_select_layer = model_args.mm_vision_select_layer
+ mm_vision_select_feature = model_args.mm_vision_select_feature
+ pretrain_mm_mlp_adapter = model_args.pretrain_mm_mlp_adapter
+ mm_patch_merge_type = model_args.mm_patch_merge_type
+
+ self.config.mm_vision_tower = vision_tower
+ self.config.vision_tower_pretrained = getattr(model_args, "vision_tower_pretrained", "")
+
+ if self.get_vision_tower() is None:
+ vision_tower = build_vision_tower(model_args)
+ vision_resampler = build_vision_resampler(model_args, vision_tower=vision_tower)
+ for k, v in vision_resampler.config.items():
+ setattr(self.config, k, v)
+
+ if fsdp is not None and len(fsdp) > 0:
+ self.vision_tower = [vision_tower]
+ self.vision_resampler = [vision_resampler]
+ else:
+ self.vision_tower = vision_tower
+ self.vision_resampler = vision_resampler
+ else:
+ if fsdp is not None and len(fsdp) > 0:
+ vision_resampler = self.vision_resampler[0]
+ vision_tower = self.vision_tower[0]
+ else:
+ vision_resampler = self.vision_resampler
+ vision_tower = self.vision_tower
+ vision_tower.load_model()
+
+ # In case it is frozen by LoRA
+ for p in self.vision_resampler.parameters():
+ p.requires_grad = True
+
+ self.config.use_mm_proj = True
+ self.config.mm_projector_type = getattr(model_args, "mm_projector_type", "linear")
+ self.config.mm_hidden_size = getattr(vision_resampler, "hidden_size", vision_tower.hidden_size)
+ self.config.mm_vision_select_layer = mm_vision_select_layer
+ self.config.mm_vision_select_feature = mm_vision_select_feature
+ self.config.mm_patch_merge_type = mm_patch_merge_type
+
+
+ if not hasattr(self.config, 'add_faster_video'):
+ if model_args.add_faster_video:
+ embed_std = 1 / torch.sqrt(torch.tensor(self.config.hidden_size, dtype=self.dtype))
+ self.faster_token = nn.Parameter(
+ torch.randn(self.config.hidden_size, dtype=self.dtype) * embed_std
+ )
+
+ if getattr(self, "mm_projector", None) is None:
+ self.mm_projector = build_vision_projector(self.config, vision_cfg=vision_tower.config)
+
+ if "unpad" in mm_patch_merge_type:
+ embed_std = 1 / torch.sqrt(torch.tensor(self.config.hidden_size, dtype=self.dtype))
+ self.image_newline = nn.Parameter(torch.randn(self.config.hidden_size, dtype=self.dtype) * embed_std)
+ else:
+ # In case it is frozen by LoRA
+ for p in self.mm_projector.parameters():
+ p.requires_grad = True
+
+ if pretrain_mm_mlp_adapter is not None:
+ mm_projector_weights = torch.load(pretrain_mm_mlp_adapter, map_location="cpu")
+
+ def get_w(weights, keyword):
+ return {k.split(keyword + ".")[1]: v for k, v in weights.items() if keyword in k}
+
+ incompatible_keys = self.mm_projector.load_state_dict(get_w(mm_projector_weights, "mm_projector"))
+ rank0_print(f"Loaded mm projector weights from {pretrain_mm_mlp_adapter}. Incompatible keys: {incompatible_keys}")
+ incompatible_keys = self.vision_resampler.load_state_dict(get_w(mm_projector_weights, "vision_resampler"), strict=False)
+ rank0_print(f"Loaded vision resampler weights from {pretrain_mm_mlp_adapter}. Incompatible keys: {incompatible_keys}")
+
+
+def unpad_image(tensor, original_size):
+ """
+ Unpads a PyTorch tensor of a padded and resized image.
+
+ Args:
+ tensor (torch.Tensor): The image tensor, assumed to be in CxHxW format.
+ original_size (tuple): The original size of the image (height, width).
+
+ Returns:
+ torch.Tensor: The unpadded image tensor.
+ """
+ original_width, original_height = original_size
+ current_height, current_width = tensor.shape[1:]
+
+ # Compute aspect ratios
+ original_aspect_ratio = original_width / original_height
+ current_aspect_ratio = current_width / current_height
+
+ # Determine padding size and direction
+ if original_aspect_ratio > current_aspect_ratio:
+ # Padding was added to the height
+ scale_factor = current_width / original_width
+ new_height = int(original_height * scale_factor)
+ padding = (current_height - new_height) // 2
+ unpadded_tensor = tensor[:, padding : current_height - padding, :]
+ else:
+ # Padding was added to the width
+ scale_factor = current_height / original_height
+ new_width = int(original_width * scale_factor)
+ padding = (current_width - new_width) // 2
+ unpadded_tensor = tensor[:, :, padding : current_width - padding]
+
+ return unpadded_tensor
+
+
+class LlavaMetaForCausalLM(ABC):
+
+ @abstractmethod
+ def get_model(self):
+ pass
+
+ def get_vision_tower(self):
+ return self.get_model().get_vision_tower()
+
+ def get_2dPool(self, image_feature, stride=2):
+ height = width = self.get_vision_tower().num_patches_per_side
+ num_frames, num_tokens, num_dim = image_feature.shape
+ image_feature = image_feature.view(num_frames, height, width, -1)
+ image_feature = image_feature.permute(0, 3, 1, 2).contiguous()
+ # image_feature = nn.functional.max_pool2d(image_feature, self.config.mm_spatial_pool_stride)
+ if self.config.mm_spatial_pool_mode == "average":
+ image_feature = nn.functional.avg_pool2d(image_feature, stride)
+ elif self.config.mm_spatial_pool_mode == "max":
+ image_feature = nn.functional.max_pool2d(image_feature, stride)
+ elif self.config.mm_spatial_pool_mode == "bilinear":
+ height, width = image_feature.shape[2:]
+ scaled_shape = [math.ceil(height / stride), math.ceil(width / stride)]
+ image_feature = nn.functional.interpolate(image_feature, size=scaled_shape, mode='bilinear')
+
+ else:
+ raise ValueError(f"Unexpected mm_spatial_pool_mode: {self.config.mm_spatial_pool_mode}")
+ image_feature = image_feature.permute(0, 2, 3, 1)
+ image_feature = image_feature.view(num_frames, -1, num_dim)
+ return image_feature
+
+ def encode_images(self, images):
+ image_features = self.get_model().get_vision_tower()(images)
+ # image_features = self.get_model().vision_resampler(image_features, images=images)
+ image_features = self.get_model().mm_projector(image_features)
+ return image_features
+
+ def encode_multimodals(self, videos_or_images, video_idx_in_batch, split_sizes=None):
+ videos_or_images_features = self.get_model().get_vision_tower()(videos_or_images)
+ per_videos_or_images_features = torch.split(videos_or_images_features, split_sizes, dim=0) # tuple, (dim_1, 576, 4096)
+ all_videos_or_images_features = []
+ all_faster_video_features = []
+ cur_mm_spatial_pool_stride = self.config.mm_spatial_pool_stride
+
+ for idx, feat in enumerate(per_videos_or_images_features):
+
+ feat = self.get_model().mm_projector(feat)
+ faster_video_feature = 0
+ slower_img_feat = 0
+ if idx in video_idx_in_batch and cur_mm_spatial_pool_stride > 1:
+ slower_img_feat = self.get_2dPool(feat,cur_mm_spatial_pool_stride)
+ if self.config.add_faster_video:
+ cur_mm_spatial_pool_stride = cur_mm_spatial_pool_stride * 2
+ faster_video_feature = self.get_2dPool(feat,cur_mm_spatial_pool_stride)
+ if slower_img_feat is not 0:
+ all_videos_or_images_features.append(slower_img_feat)
+ else:
+ all_videos_or_images_features.append(feat)
+ all_faster_video_features.append(faster_video_feature)
+ return all_videos_or_images_features,all_faster_video_features
+
+ def add_token_per_grid(self, image_feature):
+ resize_h = int(math.sqrt(image_feature.shape[1]))
+ num_frames = image_feature.shape[0]
+ feature_dim = image_feature.shape[-1]
+
+ image_feature = image_feature.view(num_frames, 1, resize_h, resize_h, -1)
+ image_feature = image_feature.permute(4, 0, 2, 1, 3).contiguous()
+ image_feature = image_feature.flatten(1, 2).flatten(2, 3)
+ image_feature = torch.cat((image_feature, self.model.image_newline[:, None, None].expand(*image_feature.shape[:-1], 1).to(image_feature.device)), dim=-1)
+ if getattr(self.config, "add_faster_video", False):
+ # import pdb; pdb.set_trace()
+ # (3584, 832, 14) -> (3584, 64, 13, 14)
+ image_feature = image_feature.view(feature_dim, num_frames,resize_h, -1)
+ # (3584, 64, 13, 14) -> (64, 13, 14, 3584)
+ image_feature = image_feature.permute(1, 2, 3, 0).contiguous()
+ # (64, 13, 14, 3584) -> (64, 13*14, 3584)
+ image_feature = image_feature.flatten(1, 2)
+ # import pdb; pdb.set_trace()
+ return image_feature
+ # import pdb; pdb.set_trace()
+ image_feature = image_feature.flatten(1, 2).transpose(0, 1)
+ return image_feature
+
+ def add_token_per_frame(self, image_feature):
+ image_feature = image_feature.permute(2, 0, 1).contiguous()
+ image_feature = torch.cat((image_feature, self.model.image_newline[:, None, None].expand(*image_feature.shape[:-1], 1).to(image_feature.device)), dim=-1)
+ image_feature = image_feature.permute(1, 2, 0).contiguous()
+ return image_feature
+
+ def prepare_inputs_labels_for_multimodal(self, input_ids, position_ids, attention_mask, past_key_values, labels, images, modalities=["image"], image_sizes=None):
+ vision_tower = self.get_vision_tower()
+ # rank_print(modalities)
+ if vision_tower is None or images is None or input_ids.shape[1] == 1:
+ return input_ids, position_ids, attention_mask, past_key_values, None, labels
+
+ if isinstance(modalities, str):
+ modalities = [modalities]
+
+ # import pdb; pdb.set_trace()
+ if type(images) is list or images.ndim == 5:
+ if type(images) is list:
+ images = [x.unsqueeze(0) if x.ndim == 3 else x for x in images]
+
+ video_idx_in_batch = []
+ for _ in range(len(modalities)):
+ if modalities[_] == "video":
+ video_idx_in_batch.append(_)
+
+ images_list = []
+ for image in images:
+ if image.ndim == 4:
+ images_list.append(image)
+ else:
+ images_list.append(image.unsqueeze(0))
+
+ concat_images = torch.cat([image for image in images_list], dim=0)
+ split_sizes = [image.shape[0] for image in images_list]
+ encoded_image_features = self.encode_images(concat_images)
+ # image_features,all_faster_video_features = self.encode_multimodals(concat_images, video_idx_in_batch, split_sizes)
+
+ # This is a list, each element is [num_images, patch * patch, dim]
+ # rank_print(f"Concat images : {concat_images.shape}")
+ encoded_image_features = torch.split(encoded_image_features, split_sizes)
+ image_features = []
+ for idx, image_feat in enumerate(encoded_image_features):
+ if idx in video_idx_in_batch:
+ image_features.append(self.get_2dPool(image_feat))
+ else:
+ image_features.append(image_feat)
+ # image_features = self.encode_multimodals(concat_images, video_idx_in_batch, split_sizes)
+ # rank_print(f"Encoded image feats : {[x.shape for x in image_features]}")
+ # image_features = torch.split(image_features, split_sizes, dim=0)
+ mm_patch_merge_type = getattr(self.config, "mm_patch_merge_type", "flat")
+ image_aspect_ratio = getattr(self.config, "image_aspect_ratio", "square")
+ mm_newline_position = getattr(self.config, "mm_newline_position", "one_token")
+
+ if mm_patch_merge_type == "flat":
+ image_features = [x.flatten(0, 1) for x in image_features]
+
+ elif mm_patch_merge_type.startswith("spatial"):
+ new_image_features = []
+ for image_idx, image_feature in enumerate(image_features):
+ # FIXME: now assume the image is square, and split to 2x2 patches
+ # num_patches = h * w, where h = w = sqrt(num_patches)
+ # currently image_feature is a tensor of shape (4, num_patches, hidden_size)
+ # we want to first unflatten it to (2, 2, h, w, hidden_size)
+ # rank0_print("At least we are reaching here")
+ # import pdb; pdb.set_trace()
+ if image_idx in video_idx_in_batch: # video operations
+ # rank0_print("Video")
+ if mm_newline_position == "grid":
+ # Grid-wise
+ image_feature = self.add_token_per_grid(image_feature)
+ if getattr(self.config, "add_faster_video", False):
+ faster_video_feature = self.add_token_per_grid(all_faster_video_features[image_idx])
+ # Add a token for each frame
+ concat_slow_fater_token = []
+ # import pdb; pdb.set_trace()
+ for _ in range(image_feature.shape[0]):
+ if _ % self.config.faster_token_stride == 0:
+ concat_slow_fater_token.append(torch.cat((image_feature[_], self.model.faster_token[None].to(image_feature.device)), dim=0))
+ else:
+ concat_slow_fater_token.append(torch.cat((faster_video_feature[_], self.model.faster_token[None].to(image_feature.device)), dim=0))
+ # import pdb; pdb.set_trace()
+ image_feature = torch.cat(concat_slow_fater_token)
+
+ # print("!!!!!!!!!!!!")
+
+ new_image_features.append(image_feature)
+ elif mm_newline_position == "frame":
+ # Frame-wise
+ image_feature = self.add_token_per_frame(image_feature)
+
+ new_image_features.append(image_feature.flatten(0, 1))
+
+ elif mm_newline_position == "one_token":
+ # one-token
+ image_feature = image_feature.flatten(0, 1)
+ if 'unpad' in mm_patch_merge_type:
+ image_feature = torch.cat((
+ image_feature,
+ self.model.image_newline[None].to(image_feature.device)
+ ), dim=0)
+ new_image_features.append(image_feature)
+ elif mm_newline_position == "no_token":
+ new_image_features.append(image_feature.flatten(0, 1))
+ else:
+ raise ValueError(f"Unexpected mm_newline_position: {mm_newline_position}")
+ elif image_feature.shape[0] > 1: # multi patches and multi images operations
+ # rank0_print("Single-images")
+ base_image_feature = image_feature[0]
+ image_feature = image_feature[1:]
+ height = width = self.get_vision_tower().num_patches_per_side
+ assert height * width == base_image_feature.shape[0]
+
+ if "anyres_max" in image_aspect_ratio:
+ matched_anyres_max_num_patches = re.match(r"anyres_max_(\d+)", image_aspect_ratio)
+ if matched_anyres_max_num_patches:
+ max_num_patches = int(matched_anyres_max_num_patches.group(1))
+
+ if image_aspect_ratio == "anyres" or "anyres_max" in image_aspect_ratio:
+ if hasattr(self.get_vision_tower(), "image_size"):
+ vision_tower_image_size = self.get_vision_tower().image_size
+ else:
+ raise ValueError("vision_tower_image_size is not found in the vision tower.")
+ try:
+ num_patch_width, num_patch_height = get_anyres_image_grid_shape(image_sizes[image_idx], self.config.image_grid_pinpoints, vision_tower_image_size)
+ except Exception as e:
+ rank0_print(f"Error: {e}")
+ num_patch_width, num_patch_height = 2, 2
+ image_feature = image_feature.view(num_patch_height, num_patch_width, height, width, -1)
+ else:
+ image_feature = image_feature.view(2, 2, height, width, -1)
+
+ if "maxpool2x2" in mm_patch_merge_type:
+ image_feature = image_feature.permute(4, 0, 2, 1, 3).contiguous()
+ image_feature = image_feature.flatten(1, 2).flatten(2, 3)
+ image_feature = nn.functional.max_pool2d(image_feature, 2)
+ image_feature = image_feature.flatten(1, 2).transpose(0, 1)
+ elif "unpad" in mm_patch_merge_type and "anyres_max" in image_aspect_ratio and matched_anyres_max_num_patches:
+ unit = image_feature.shape[2]
+ image_feature = image_feature.permute(4, 0, 2, 1, 3).contiguous()
+ image_feature = image_feature.flatten(1, 2).flatten(2, 3)
+ image_feature = unpad_image(image_feature, image_sizes[image_idx])
+ c, h, w = image_feature.shape
+ times = math.sqrt(h * w / (max_num_patches * unit**2))
+ if times > 1.1:
+ image_feature = image_feature[None]
+ image_feature = nn.functional.interpolate(image_feature, [int(h // times), int(w // times)], mode="bilinear")[0]
+ image_feature = torch.cat((image_feature, self.model.image_newline[:, None, None].expand(*image_feature.shape[:-1], 1).to(image_feature.device)), dim=-1)
+ image_feature = image_feature.flatten(1, 2).transpose(0, 1)
+ elif "unpad" in mm_patch_merge_type:
+ image_feature = image_feature.permute(4, 0, 2, 1, 3).contiguous()
+ image_feature = image_feature.flatten(1, 2).flatten(2, 3)
+ image_feature = unpad_image(image_feature, image_sizes[image_idx])
+ image_feature = torch.cat((image_feature, self.model.image_newline[:, None, None].expand(*image_feature.shape[:-1], 1).to(image_feature.device)), dim=-1)
+ image_feature = image_feature.flatten(1, 2).transpose(0, 1)
+ else:
+ image_feature = image_feature.permute(0, 2, 1, 3, 4).contiguous()
+ image_feature = image_feature.flatten(0, 3)
+ if "nobase" in mm_patch_merge_type:
+ pass
+ else:
+ image_feature = torch.cat((base_image_feature, image_feature), dim=0)
+ new_image_features.append(image_feature)
+ else: # single image operations
+ image_feature = image_feature[0]
+ if "unpad" in mm_patch_merge_type:
+ image_feature = torch.cat((image_feature, self.model.image_newline[None]), dim=0)
+
+ new_image_features.append(image_feature)
+ image_features = new_image_features
+ else:
+ raise ValueError(f"Unexpected mm_patch_merge_type: {self.config.mm_patch_merge_type}")
+ else:
+ image_features = self.encode_images(images)
+
+ # TODO: image start / end is not implemented here to support pretraining.
+ if getattr(self.config, "tune_mm_mlp_adapter", False) and getattr(self.config, "mm_use_im_start_end", False):
+ raise NotImplementedError
+ # rank_print(f"Total images : {len(image_features)}")
+
+ # Let's just add dummy tensors if they do not exist,
+ # it is a headache to deal with None all the time.
+ # But it is not ideal, and if you have a better idea,
+ # please open an issue / submit a PR, thanks.
+ _labels = labels
+ _position_ids = position_ids
+ _attention_mask = attention_mask
+ if attention_mask is None:
+ attention_mask = torch.ones_like(input_ids, dtype=torch.bool)
+ else:
+ attention_mask = attention_mask.bool()
+ if position_ids is None:
+ position_ids = torch.arange(0, input_ids.shape[1], dtype=torch.long, device=input_ids.device)
+ if labels is None:
+ labels = torch.full_like(input_ids, IGNORE_INDEX)
+
+ # remove the padding using attention_mask -- FIXME
+ _input_ids = input_ids
+ input_ids = [cur_input_ids[cur_attention_mask] for cur_input_ids, cur_attention_mask in zip(input_ids, attention_mask)]
+ labels = [cur_labels[cur_attention_mask] for cur_labels, cur_attention_mask in zip(labels, attention_mask)]
+
+ new_input_embeds = []
+ new_labels = []
+ cur_image_idx = 0
+ # rank_print("Inserting Images embedding")
+ for batch_idx, cur_input_ids in enumerate(input_ids):
+ num_images = (cur_input_ids == IMAGE_TOKEN_INDEX).sum()
+ # rank0_print(num_images)
+ if num_images == 0:
+ cur_image_features = image_features[cur_image_idx]
+ cur_input_embeds_1 = self.get_model().embed_tokens(cur_input_ids)
+ cur_input_embeds = torch.cat([cur_input_embeds_1, cur_image_features[0:0]], dim=0)
+ new_input_embeds.append(cur_input_embeds)
+ new_labels.append(labels[batch_idx])
+ cur_image_idx += 1
+ continue
+
+ image_token_indices = [-1] + torch.where(cur_input_ids == IMAGE_TOKEN_INDEX)[0].tolist() + [cur_input_ids.shape[0]]
+ cur_input_ids_noim = []
+ cur_labels = labels[batch_idx]
+ cur_labels_noim = []
+ for i in range(len(image_token_indices) - 1):
+ cur_input_ids_noim.append(cur_input_ids[image_token_indices[i] + 1 : image_token_indices[i + 1]])
+ cur_labels_noim.append(cur_labels[image_token_indices[i] + 1 : image_token_indices[i + 1]])
+ split_sizes = [x.shape[0] for x in cur_labels_noim]
+ cur_input_embeds = self.get_model().embed_tokens(torch.cat(cur_input_ids_noim))
+ cur_input_embeds_no_im = torch.split(cur_input_embeds, split_sizes, dim=0)
+ cur_new_input_embeds = []
+ cur_new_labels = []
+
+ for i in range(num_images + 1):
+ cur_new_input_embeds.append(cur_input_embeds_no_im[i])
+ cur_new_labels.append(cur_labels_noim[i])
+ if i < num_images:
+ try:
+ cur_image_features = image_features[cur_image_idx]
+ except IndexError:
+ cur_image_features = image_features[cur_image_idx - 1]
+ cur_image_idx += 1
+ cur_new_input_embeds.append(cur_image_features)
+ cur_new_labels.append(torch.full((cur_image_features.shape[0],), IGNORE_INDEX, device=cur_labels.device, dtype=cur_labels.dtype))
+
+ cur_new_input_embeds = [x.to(self.device) for x in cur_new_input_embeds]
+
+ # import pdb; pdb.set_trace()
+ cur_new_input_embeds = torch.cat(cur_new_input_embeds)
+ cur_new_labels = torch.cat(cur_new_labels)
+
+ new_input_embeds.append(cur_new_input_embeds)
+ new_labels.append(cur_new_labels)
+
+ # Truncate sequences to max length as image embeddings can make the sequence longer
+ tokenizer_model_max_length = getattr(self.config, "tokenizer_model_max_length", None)
+ # rank_print("Finishing Inserting")
+
+ new_input_embeds = [x[:tokenizer_model_max_length] for x, modality in zip(new_input_embeds, modalities)]
+ new_labels = [x[:tokenizer_model_max_length] for x, modality in zip(new_labels, modalities)]
+ # TODO: Hard code for control loss spike
+ # if tokenizer_model_max_length is not None:
+ # new_input_embeds = [x[:4096] if modality != "video" else x[:tokenizer_model_max_length] for x, modality in zip(new_input_embeds, modalities)]
+ # new_labels = [x[:4096] if modality != "video" else x[:tokenizer_model_max_length] for x, modality in zip(new_labels, modalities)]
+
+ # Combine them
+ max_len = max(x.shape[0] for x in new_input_embeds)
+ batch_size = len(new_input_embeds)
+
+ new_input_embeds_padded = []
+ new_labels_padded = torch.full((batch_size, max_len), IGNORE_INDEX, dtype=new_labels[0].dtype, device=new_labels[0].device)
+ attention_mask = torch.zeros((batch_size, max_len), dtype=attention_mask.dtype, device=attention_mask.device)
+ position_ids = torch.zeros((batch_size, max_len), dtype=position_ids.dtype, device=position_ids.device)
+ # rank0_print("Prepare pos id")
+
+ for i, (cur_new_embed, cur_new_labels) in enumerate(zip(new_input_embeds, new_labels)):
+ cur_len = cur_new_embed.shape[0]
+ if getattr(self.config, "tokenizer_padding_side", "right") == "left":
+ new_input_embeds_padded.append(torch.cat((torch.zeros((max_len - cur_len, cur_new_embed.shape[1]), dtype=cur_new_embed.dtype, device=cur_new_embed.device), cur_new_embed), dim=0))
+ if cur_len > 0:
+ new_labels_padded[i, -cur_len:] = cur_new_labels
+ attention_mask[i, -cur_len:] = True
+ position_ids[i, -cur_len:] = torch.arange(0, cur_len, dtype=position_ids.dtype, device=position_ids.device)
+ else:
+ new_input_embeds_padded.append(torch.cat((cur_new_embed, torch.zeros((max_len - cur_len, cur_new_embed.shape[1]), dtype=cur_new_embed.dtype, device=cur_new_embed.device)), dim=0))
+ if cur_len > 0:
+ new_labels_padded[i, :cur_len] = cur_new_labels
+ attention_mask[i, :cur_len] = True
+ position_ids[i, :cur_len] = torch.arange(0, cur_len, dtype=position_ids.dtype, device=position_ids.device)
+
+ new_input_embeds = torch.stack(new_input_embeds_padded, dim=0)
+ # rank0_print("tokenizer padding")
+
+ if _labels is None:
+ new_labels = None
+ else:
+ new_labels = new_labels_padded
+
+ if _attention_mask is None:
+ attention_mask = None
+ else:
+ attention_mask = attention_mask.to(dtype=_attention_mask.dtype)
+
+ if _position_ids is None:
+ position_ids = None
+ if getattr(self.config, "use_pos_skipping", False) and self.training:
+ position_ids = torch.arange(new_input_embeds.size(1), device=new_input_embeds.device).unsqueeze(0).to(new_input_embeds.device)
+ split_position = random.randint(0, new_input_embeds.size(1))
+ left_add = random.randint(0, self.config.pos_skipping_range)
+ right_add = random.randint(left_add, self.config.pos_skipping_range)
+ position_ids[:, :split_position] += left_add
+ position_ids[:, split_position:] += right_add
+ # import pdb; pdb.set_trace()
+ # rank0_print("Finish preparing")
+ return None, position_ids, attention_mask, past_key_values, new_input_embeds, new_labels
+
+ def initialize_vision_tokenizer(self, model_args, tokenizer):
+ if model_args.mm_use_im_patch_token:
+ tokenizer.add_tokens([DEFAULT_IMAGE_PATCH_TOKEN], special_tokens=True)
+ self.resize_token_embeddings(len(tokenizer))
+
+ if model_args.mm_use_im_start_end:
+ num_new_tokens = tokenizer.add_tokens([DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN], special_tokens=True)
+ self.resize_token_embeddings(len(tokenizer))
+
+ if num_new_tokens > 0:
+ input_embeddings = self.get_input_embeddings().weight.data
+ output_embeddings = self.get_output_embeddings().weight.data
+
+ input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
+ output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
+
+ input_embeddings[-num_new_tokens:] = input_embeddings_avg
+ output_embeddings[-num_new_tokens:] = output_embeddings_avg
+
+ if model_args.tune_mm_mlp_adapter:
+ for p in self.get_input_embeddings().parameters():
+ p.requires_grad = True
+ for p in self.get_output_embeddings().parameters():
+ p.requires_grad = False
+
+ if model_args.pretrain_mm_mlp_adapter:
+ mm_projector_weights = torch.load(model_args.pretrain_mm_mlp_adapter, map_location="cpu")
+ embed_tokens_weight = mm_projector_weights["model.embed_tokens.weight"]
+ assert num_new_tokens == 2
+ if input_embeddings.shape == embed_tokens_weight.shape:
+ input_embeddings[-num_new_tokens:] = embed_tokens_weight[-num_new_tokens:]
+ elif embed_tokens_weight.shape[0] == num_new_tokens:
+ input_embeddings[-num_new_tokens:] = embed_tokens_weight
+ else:
+ raise ValueError(f"Unexpected embed_tokens_weight shape. Pretrained: {embed_tokens_weight.shape}. Current: {input_embeddings.shape}. Numer of new tokens: {num_new_tokens}.")
+ elif model_args.mm_use_im_patch_token:
+ if model_args.tune_mm_mlp_adapter:
+ for p in self.get_input_embeddings().parameters():
+ p.requires_grad = False
+ for p in self.get_output_embeddings().parameters():
+ p.requires_grad = False
diff --git a/LLaVA-NeXT/llava/model/make_delta.py b/LLaVA-NeXT/llava/model/make_delta.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b3fbabe19506d5710dbc194db4000fee62c712d
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/make_delta.py
@@ -0,0 +1,52 @@
+"""
+Usage:
+python3 -m llava.model.make_delta --base ~/model_weights/llama-7b --target ~/model_weights/llava-7b --delta ~/model_weights/llava-7b-delta --hub-repo-id liuhaotian/llava-7b-delta
+"""
+
+import argparse
+
+import torch
+from tqdm import tqdm
+from transformers import AutoTokenizer, AutoModelForCausalLM
+from llava.model.utils import auto_upgrade
+
+
+def make_delta(base_model_path, target_model_path, delta_path, hub_repo_id):
+ print("Loading base model")
+ base = AutoModelForCausalLM.from_pretrained(base_model_path, torch_dtype=torch.float16, low_cpu_mem_usage=True)
+
+ print("Loading target model")
+ auto_upgrade(target_model_path)
+ target = AutoModelForCausalLM.from_pretrained(target_model_path, torch_dtype=torch.float16, low_cpu_mem_usage=True)
+
+ print("Calculating delta")
+ for name, param in tqdm(target.state_dict().items(), desc="Calculating delta"):
+ if name not in base.state_dict():
+ assert name in ["model.mm_projector.weight", "model.mm_projector.bias"], f"{name} not in base model"
+ continue
+ if param.data.shape == base.state_dict()[name].shape:
+ param.data -= base.state_dict()[name]
+ else:
+ assert name in ["model.embed_tokens.weight", "lm_head.weight"], f"{name} dimension mismatch: {param.data.shape} vs {base.state_dict()[name].shape}"
+ bparam = base.state_dict()[name]
+ param.data[: bparam.shape[0], : bparam.shape[1]] -= bparam
+
+ print("Saving delta")
+ if hub_repo_id:
+ kwargs = {"push_to_hub": True, "repo_id": hub_repo_id}
+ else:
+ kwargs = {}
+ target.save_pretrained(delta_path, **kwargs)
+ target_tokenizer = AutoTokenizer.from_pretrained(target_model_path)
+ target_tokenizer.save_pretrained(delta_path, **kwargs)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--base-model-path", type=str, required=True)
+ parser.add_argument("--target-model-path", type=str, required=True)
+ parser.add_argument("--delta-path", type=str, required=True)
+ parser.add_argument("--hub-repo-id", type=str, default=None)
+ args = parser.parse_args()
+
+ make_delta(args.base_model_path, args.target_model_path, args.delta_path, args.hub_repo_id)
diff --git a/LLaVA-NeXT/llava/model/multimodal_resampler/builder.py b/LLaVA-NeXT/llava/model/multimodal_resampler/builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a4b207f3bded33b89ddef3899233c3825d91701
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/multimodal_resampler/builder.py
@@ -0,0 +1,34 @@
+import torch
+
+from .masked_drop import MaskedDrop
+from .spatial_pool import SpatialPool
+from .perceiver import PerceiverResampler
+from .qformer import Qformer
+
+
+class IdentityMap(torch.nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x, *args, **kwargs):
+ return x
+
+ @property
+ def config(self):
+ return {"mm_resampler_type": None}
+
+
+def build_vision_resampler(model_args, delay_load=False, **kwargs):
+ resampler_type = getattr(model_args, "mm_resampler_type", None)
+ if resampler_type == "masked_drop":
+ return MaskedDrop(model_args)
+ elif resampler_type == "spatial_pool":
+ return SpatialPool(model_args, **kwargs)
+ elif resampler_type == "perceiver":
+ return PerceiverResampler(model_args, **kwargs)
+ elif resampler_type == "qformer":
+ return Qformer(model_args, **kwargs)
+ elif resampler_type is None:
+ return IdentityMap()
+
+ raise ValueError(f"Unknown resampler type: {resampler_type}")
diff --git a/LLaVA-NeXT/llava/model/multimodal_resampler/masked_drop.py b/LLaVA-NeXT/llava/model/multimodal_resampler/masked_drop.py
new file mode 100644
index 0000000000000000000000000000000000000000..03f0bf0b259ff5d96adea4aad91ee5498d459030
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/multimodal_resampler/masked_drop.py
@@ -0,0 +1,80 @@
+import torch
+import torch.nn as nn
+
+import random
+
+
+class MaskedDrop(nn.Module):
+ def __init__(self, model_args):
+ super().__init__()
+
+ self.mode = model_args.mm_mask_drop_mode
+ self.skip_percentage = model_args.mm_mask_drop_skip_percentage
+ self.ratio = model_args.mm_mask_drop_ratio
+ self.ratio_upper = model_args.mm_mask_drop_ratio_upper
+ self.ratio_lower = model_args.mm_mask_drop_ratio_lower
+
+ def forward(self, image_features, *args, **kwargs):
+
+ if not self.training:
+ return image_features
+
+ if self.skip_percentage > random.random():
+ return image_features
+
+ masked_features = []
+
+ for image_feature in image_features:
+ num_tokens = image_feature.shape[0]
+ if self.mode == "fixed":
+ num_keep = int(num_tokens * self.ratio)
+ masked_features.append(self.random_masking(image_feature.unsqueeze(0), num_keep)[0][0])
+ elif self.mode == "range":
+ num_keep = int(num_tokens * random.uniform(self.ratio_lower, self.ratio_upper))
+ masked_features.append(self.random_masking(image_feature.unsqueeze(0), num_keep)[0])
+ elif self.mode == "cls_only":
+ masked_features.append(image_feature[0:1])
+ else:
+ raise ValueError(f"Unexpected masked drop mode: {self.mode}")
+
+ if self.mode not in ["range"] and (type(image_features) is not list or self.mode in ["cls_only"]):
+ masked_features = torch.stack(masked_features, dim=0)
+
+ return masked_features
+
+ @property
+ def config(self):
+ return {
+ "mm_resampler_type": "masked_drop",
+ "mm_mask_drop_mode": self.mode,
+ "mm_mask_drop_skip_percentage": self.skip_percentage,
+ "mm_mask_drop_ratio": self.ratio,
+ "mm_mask_drop_ratio_upper": self.ratio_upper,
+ "mm_mask_drop_ratio_lower": self.ratio_lower,
+ }
+
+ def random_masking(self, x, len_keep):
+ """
+ Perform per-sample random masking by per-sample shuffling.
+ Per-sample shuffling is done by argsort random noise.
+ x: [N, L, D], sequence
+ """
+ N, L, D = x.shape # batch, length, dim
+
+ noise = torch.rand(N, L, device=x.device) # noise in [0, 1]
+
+ # sort noise for each sample
+ ids_shuffle = torch.argsort(noise, dim=1) # ascend: small is keep, large is remove
+ ids_restore = torch.argsort(ids_shuffle, dim=1)
+
+ # keep the first subset
+ ids_keep = ids_shuffle[:, :len_keep]
+ x_masked = torch.gather(x, dim=1, index=ids_keep.unsqueeze(-1).repeat(1, 1, D))
+
+ # generate the binary mask: 0 is keep, 1 is remove
+ mask = torch.ones([N, L], device=x.device)
+ mask[:, :len_keep] = 0
+ # unshuffle to get the binary mask
+ mask = torch.gather(mask, dim=1, index=ids_restore)
+
+ return x_masked, mask, ids_restore
diff --git a/LLaVA-NeXT/llava/model/multimodal_resampler/perceiver.py b/LLaVA-NeXT/llava/model/multimodal_resampler/perceiver.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6b17a559b2225832c7d87c4fb6894617779b9c1
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/multimodal_resampler/perceiver.py
@@ -0,0 +1,155 @@
+"""
+Taken from https://github.com/lucidrains/flamingo-pytorch
+"""
+
+import torch
+from einops import rearrange, repeat
+
+try:
+ from einops_exts import rearrange_many
+except:
+ pass
+
+from torch import einsum, nn
+
+
+def exists(val):
+ return val is not None
+
+
+def FeedForward(dim, mult=4):
+ inner_dim = int(dim * mult)
+ return nn.Sequential(
+ nn.LayerNorm(dim),
+ nn.Linear(dim, inner_dim, bias=False),
+ nn.GELU(),
+ nn.Linear(inner_dim, dim, bias=False),
+ )
+
+
+class PerceiverAttention(nn.Module):
+ def __init__(self, *, dim, dim_head=64, heads=8):
+ super().__init__()
+ self.scale = dim_head**-0.5
+ self.heads = heads
+ inner_dim = dim_head * heads
+
+ self.norm_media = nn.LayerNorm(dim)
+ self.norm_latents = nn.LayerNorm(dim)
+
+ self.to_q = nn.Linear(dim, inner_dim, bias=False)
+ self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False)
+ self.to_out = nn.Linear(inner_dim, dim, bias=False)
+
+ def forward(self, x, latents):
+ """
+ Args:
+ x (torch.Tensor): image features
+ shape (b, T, n1, D)
+ latent (torch.Tensor): latent features
+ shape (b, T, n2, D)
+ """
+ x = self.norm_media(x)
+ latents = self.norm_latents(latents)
+
+ h = self.heads
+
+ q = self.to_q(latents)
+ kv_input = torch.cat((x, latents), dim=-2)
+ k, v = self.to_kv(kv_input).chunk(2, dim=-1)
+ q, k, v = rearrange_many((q, k, v), "b t n (h d) -> b h t n d", h=h)
+ q = q * self.scale
+
+ # attention
+ sim = einsum("... i d, ... j d -> ... i j", q, k)
+ sim = sim - sim.amax(dim=-1, keepdim=True).detach()
+ attn = sim.softmax(dim=-1)
+
+ out = einsum("... i j, ... j d -> ... i d", attn, v)
+ out = rearrange(out, "b h t n d -> b t n (h d)", h=h)
+ return self.to_out(out)
+
+
+class PerceiverResamplerModule(nn.Module):
+ def __init__(
+ self,
+ *,
+ dim,
+ depth=6,
+ dim_head=64,
+ heads=8,
+ num_latents=64,
+ max_num_media=None,
+ max_num_frames=None,
+ ff_mult=4,
+ ):
+ super().__init__()
+ self.latents = nn.Parameter(torch.randn(num_latents, dim))
+ self.frame_embs = nn.Parameter(torch.randn(max_num_frames, dim)) if exists(max_num_frames) else None
+ self.media_time_embs = nn.Parameter(torch.randn(max_num_media, 1, dim)) if exists(max_num_media) else None
+
+ self.layers = nn.ModuleList([])
+ for _ in range(depth):
+ self.layers.append(
+ nn.ModuleList(
+ [
+ PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads),
+ FeedForward(dim=dim, mult=ff_mult) if ff_mult > 0 else nn.Identity(),
+ ]
+ )
+ )
+
+ self.norm = nn.LayerNorm(dim)
+
+ def forward(self, x):
+ """
+ Args:
+ x (torch.Tensor): image features
+ shape (b, T, F, v, D)
+ Returns:
+ shape (b, T, n, D) where n is self.num_latents
+ """
+ b, T, F, v = x.shape[:4]
+
+ # frame and media time embeddings
+ if exists(self.frame_embs):
+ frame_embs = repeat(self.frame_embs[:F], "F d -> b T F v d", b=b, T=T, v=v)
+ x = x + frame_embs
+ x = rearrange(x, "b T F v d -> b T (F v) d") # flatten the frame and spatial dimensions
+ if exists(self.media_time_embs):
+ x = x + self.media_time_embs[:T]
+
+ # blocks
+ latents = repeat(self.latents, "n d -> b T n d", b=b, T=T)
+ for attn, ff in self.layers:
+ latents = attn(x, latents) + latents
+ latents = ff(latents) + latents
+ return self.norm(latents)
+
+
+class PerceiverResampler(nn.Module):
+ def __init__(self, model_args, vision_tower):
+ super().__init__()
+
+ self.depth = model_args.mm_perceiver_depth
+ self.num_latents = model_args.mm_perceiver_latents
+ self.ff_mult = model_args.mm_perceiver_ff_mult
+ self.pretrained = model_args.mm_perceiver_pretrained
+
+ self.perceiver = PerceiverResamplerModule(dim=vision_tower.hidden_size, depth=self.depth, num_latents=self.num_latents, ff_mult=self.ff_mult)
+
+ if self.pretrained is not None:
+ self.load_state_dict(torch.load(self.pretrained))
+
+ def forward(self, image_features, *args, **kwargs):
+ return self.perceiver(image_features[:, None, None]).squeeze(1)
+
+ @property
+ def config(self):
+ return {
+ "mm_resampler_type": "perceiver",
+ "mm_perceiver_depth": self.depth,
+ "mm_perceiver_latents": self.num_latents,
+ "mm_perceiver_ff_mult": self.ff_mult,
+ "mm_perceiver_pretrained": self.pretrained,
+ }
diff --git a/LLaVA-NeXT/llava/model/utils.py b/LLaVA-NeXT/llava/model/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..10652a5f9aaa2e0cddaef0b1a7bc39013a0d957b
--- /dev/null
+++ b/LLaVA-NeXT/llava/model/utils.py
@@ -0,0 +1,20 @@
+from transformers import AutoConfig
+
+
+def auto_upgrade(config):
+ cfg = AutoConfig.from_pretrained(config)
+ if "llava" in config and "llava" not in cfg.model_type:
+ assert cfg.model_type == "llama"
+ print("You are using newer LLaVA code base, while the checkpoint of v0 is from older code base.")
+ print("You must upgrade the checkpoint to the new code base (this can be done automatically).")
+ confirm = input("Please confirm that you want to upgrade the checkpoint. [Y/N]")
+ if confirm.lower() in ["y", "yes"]:
+ print("Upgrading checkpoint...")
+ assert len(cfg.architectures) == 1
+ setattr(cfg.__class__, "model_type", "llava")
+ cfg.architectures[0] = "LlavaLlamaForCausalLM"
+ cfg.save_pretrained(config)
+ print("Checkpoint upgraded.")
+ else:
+ print("Checkpoint upgrade aborted.")
+ exit(1)
diff --git a/LLaVA-NeXT/llava/utils.py b/LLaVA-NeXT/llava/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e004c33bc503d213df619dec948f39b4c13e53d
--- /dev/null
+++ b/LLaVA-NeXT/llava/utils.py
@@ -0,0 +1,198 @@
+import datetime
+import logging
+import logging.handlers
+import os
+import sys
+import numpy as np
+
+import requests
+
+from llava.constants import LOGDIR
+
+server_error_msg = "**NETWORK ERROR DUE TO HIGH TRAFFIC. PLEASE REGENERATE OR REFRESH THIS PAGE.**"
+moderation_msg = "I am sorry. Your input may violate our content moderation guidelines. Please avoid using harmful or offensive content."
+
+handler = None
+
+import torch.distributed as dist
+
+try:
+ import av
+ from decord import VideoReader, cpu
+except ImportError:
+ print("Please install pyav to use video processing functions.")
+
+def process_video_with_decord(video_file, data_args):
+ vr = VideoReader(video_file, ctx=cpu(0), num_threads=1)
+ total_frame_num = len(vr)
+ video_time = total_frame_num / vr.get_avg_fps()
+ avg_fps = round(vr.get_avg_fps() / data_args.video_fps)
+ frame_idx = [i for i in range(0, total_frame_num, avg_fps)]
+ frame_time = [i/avg_fps for i in frame_idx]
+
+
+ if data_args.frames_upbound > 0:
+ if len(frame_idx) > data_args.frames_upbound or data_args.force_sample:
+ uniform_sampled_frames = np.linspace(0, total_frame_num - 1, data_args.frames_upbound, dtype=int)
+ frame_idx = uniform_sampled_frames.tolist()
+ frame_time = [i/vr.get_avg_fps() for i in frame_idx]
+
+ video = vr.get_batch(frame_idx).asnumpy()
+ frame_time = ",".join([f"{i:.2f}s" for i in frame_time])
+
+ num_frames_to_sample = num_frames = len(frame_idx)
+ # https://github.com/dmlc/decord/issues/208
+ vr.seek(0)
+ return video, video_time, frame_time, num_frames_to_sample
+
+def process_video_with_pyav(video_file, data_args):
+ container = av.open(video_file)
+ # !!! This is the only difference. Using auto threading
+ container.streams.video[0].thread_type = "AUTO"
+
+ video_frames = []
+ for packet in container.demux():
+ if packet.stream.type == 'video':
+ for frame in packet.decode():
+ video_frames.append(frame)
+ total_frame_num = len(video_frames)
+ video_time = video_frames[-1].time
+ avg_fps = round(total_frame_num / video_time / data_args.video_fps)
+ frame_idx = [i for i in range(0, total_frame_num, avg_fps)]
+
+ if data_args.frames_upbound > 0:
+ if len(frame_idx) > data_args.frames_upbound:
+ uniform_sampled_frames = np.linspace(0, total_frame_num - 1, data_args.frames_upbound, dtype=int)
+ frame_idx = uniform_sampled_frames.tolist()
+
+
+ frames = [video_frames[i] for i in frame_idx]
+ return np.stack([x.to_ndarray(format="rgb24") for x in frames])
+
+
+def rank0_print(*args):
+ if dist.is_initialized():
+ if dist.get_rank() == 0:
+ print(f"Rank {dist.get_rank()}: ", *args)
+ else:
+ print(*args)
+
+
+def rank_print(*args):
+ if dist.is_initialized():
+ print(f"Rank {dist.get_rank()}: ", *args)
+ else:
+ print(*args)
+
+def build_logger(logger_name, logger_filename):
+ global handler
+
+ formatter = logging.Formatter(
+ fmt="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+
+ # Set the format of root handlers
+ if not logging.getLogger().handlers:
+ logging.basicConfig(level=logging.INFO)
+ logging.getLogger().handlers[0].setFormatter(formatter)
+
+ # Redirect stdout and stderr to loggers
+ stdout_logger = logging.getLogger("stdout")
+ stdout_logger.setLevel(logging.INFO)
+ sl = StreamToLogger(stdout_logger, logging.INFO)
+ sys.stdout = sl
+
+ stderr_logger = logging.getLogger("stderr")
+ stderr_logger.setLevel(logging.ERROR)
+ sl = StreamToLogger(stderr_logger, logging.ERROR)
+ sys.stderr = sl
+
+ # Get logger
+ logger = logging.getLogger(logger_name)
+ logger.setLevel(logging.INFO)
+
+ # Add a file handler for all loggers
+ if handler is None:
+ os.makedirs(LOGDIR, exist_ok=True)
+ filename = os.path.join(LOGDIR, logger_filename)
+ handler = logging.handlers.TimedRotatingFileHandler(filename, when="D", utc=True)
+ handler.setFormatter(formatter)
+
+ for name, item in logging.root.manager.loggerDict.items():
+ if isinstance(item, logging.Logger):
+ item.addHandler(handler)
+
+ return logger
+
+
+class StreamToLogger(object):
+ """
+ Fake file-like stream object that redirects writes to a logger instance.
+ """
+
+ def __init__(self, logger, log_level=logging.INFO):
+ self.terminal = sys.stdout
+ self.logger = logger
+ self.log_level = log_level
+ self.linebuf = ""
+
+ def __getattr__(self, attr):
+ return getattr(self.terminal, attr)
+
+ def write(self, buf):
+ temp_linebuf = self.linebuf + buf
+ self.linebuf = ""
+ for line in temp_linebuf.splitlines(True):
+ # From the io.TextIOWrapper docs:
+ # On output, if newline is None, any '\n' characters written
+ # are translated to the system default line separator.
+ # By default sys.stdout.write() expects '\n' newlines and then
+ # translates them so this is still cross platform.
+ if line[-1] == "\n":
+ self.logger.log(self.log_level, line.rstrip())
+ else:
+ self.linebuf += line
+
+ def flush(self):
+ if self.linebuf != "":
+ self.logger.log(self.log_level, self.linebuf.rstrip())
+ self.linebuf = ""
+
+
+def disable_torch_init():
+ """
+ Disable the redundant torch default initialization to accelerate model creation.
+ """
+ import torch
+
+ setattr(torch.nn.Linear, "reset_parameters", lambda self: None)
+ setattr(torch.nn.LayerNorm, "reset_parameters", lambda self: None)
+
+
+def violates_moderation(text):
+ """
+ Check whether the text violates OpenAI moderation API.
+ """
+ url = "https://api.openai.com/v1/moderations"
+ headers = {"Content-Type": "application/json", "Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]}
+ text = text.replace("\n", "")
+ data = "{" + '"input": ' + f'"{text}"' + "}"
+ data = data.encode("utf-8")
+ try:
+ ret = requests.post(url, headers=headers, data=data, timeout=5)
+ flagged = ret.json()["results"][0]["flagged"]
+ except requests.exceptions.RequestException as e:
+ print(f"######################### Moderation Error: {e} #########################")
+ flagged = False
+ except KeyError as e:
+ print(f"######################### Moderation Error: {e} #########################")
+ flagged = False
+
+ return flagged
+
+
+def pretty_print_semaphore(semaphore):
+ if semaphore is None:
+ return "None"
+ return f"Semaphore(value={semaphore._value}, locked={semaphore.locked()})"
diff --git a/sample/dream_sample.py b/sample/dream_sample.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8050e3c5827f36692554547d11cfa9ba5954f8e
--- /dev/null
+++ b/sample/dream_sample.py
@@ -0,0 +1,691 @@
+from __future__ import annotations
+import math, json, os, time
+from dataclasses import dataclass
+from typing import List, Tuple
+import numpy as np
+from jinja2 import Template
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+import transformers
+from transformers import AutoTokenizer, AutoModel
+import multiprocessing as mp
+
+
+from dream import DreamTokenizer
+from dream.modeling_dream import DreamModel
+from dream.generation_utils_block import DreamGenerationMixin
+import types
+from dream.generation_utils_block import DreamGenerationConfig
+
+
+from transformers.utils import ModelOutput
+from typing import Any, Dict, Optional, Tuple, Union
+import torch.distributions as dists
+from dataclasses import dataclass
+from torch.nn import functional as F
+import torch
+
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def top_p_logits(logits, top_p=None):
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
+ sorted_indices_to_remove = cumulative_probs > top_p
+ # Shift the indices to the right to keep the first token above the threshold
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
+ sorted_indices_to_remove[..., 0] = 0
+
+ mask = torch.zeros_like(logits, dtype=torch.bool, device=logits.device)
+ mask = mask.scatter_(-1, sorted_indices, sorted_indices_to_remove)
+ logits = logits.masked_fill(mask, torch.finfo(logits.dtype).min)
+ return logits
+
+def top_k_logits(logits, top_k=None):
+ top_k = min(top_k, logits.size(-1)) # Safety check
+ # Remove all tokens with a probability less than the last token of the top-k
+ indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
+ logits = logits.masked_fill(indices_to_remove, torch.finfo(logits.dtype).min)
+ return logits
+
+
+def sample_tokens(logits, temperature=0.0, top_p=None, top_k=None, tar=None):
+
+ logits = logits.float()
+
+ if temperature > 0:
+ logits = logits / temperature
+ if top_p is not None and top_p < 1:
+ logits = top_p_logits(logits, top_p)
+ if top_k is not None:
+ logits = top_k_logits(logits, top_k)
+
+ dist = dists.Categorical(logits=logits)
+ x0 = dist.sample()
+ probs = dist.probs
+
+ if temperature > 0:
+ target = probs.gather(-1, x0.unsqueeze(-1)).squeeze(-1)
+ else:
+ target, x0 = probs.max(dim=-1)
+
+ if tar == "confidence":
+ return target, x0
+
+ if tar == "margin_confidence":
+ sorted_probs, _ = torch.sort(probs, dim=-1, descending=True)
+ # Extract top1 and top2 probabilities
+ top1_probs = sorted_probs[:, 0]
+
+ top2_probs = sorted_probs[:, 1]
+ # Calculate confidence as top1 - top2
+ target = top1_probs - top2_probs
+
+ if tar == "neg_entropy":
+ epsilon = 1e-10
+ log_probs = torch.log(probs + epsilon)
+ target = torch.sum(probs * log_probs, dim=-1)
+
+ return target, x0
+
+
+@dataclass
+class DreamModelOutput(ModelOutput):
+ sequences: torch.LongTensor = None
+ history: Optional[Tuple[torch.FloatTensor]] = None
+
+
+@torch.no_grad()
+def _sample(
+ model,
+ input_ids: torch.LongTensor,
+ attention_mask: Optional[torch.LongTensor],
+ generation_config: DreamGenerationConfig,
+ block_length: Optional[int] = 32,
+ use_cache: bool = False,
+ further_horizon: int = 128,
+ mask_token_id: int = 151666,
+ eos_token_id: int = 151645,
+ pad_token_id: int = 151643,
+ pad_target_penalty: float = 1.0,
+ unmask_threshold: float = 0.9
+) -> Union[DreamModelOutput, torch.LongTensor]:
+ # init values
+
+ output_history = generation_config.output_history
+ return_dict_in_generate = generation_config.return_dict_in_generate
+ max_length = generation_config.max_length
+ steps = generation_config.steps
+ temperature = generation_config.temperature
+ top_p = generation_config.top_p
+ top_k = generation_config.top_k
+ tar = generation_config.tar
+ alg_temp = generation_config.alg_temp
+ cgws = further_horizon
+
+
+ histories = [] if (return_dict_in_generate and output_history) else None
+
+ # pad input_ids to max_length
+ x = F.pad(input_ids, (0, max_length - input_ids.shape[1]), value=mask_token_id)
+ gen_length = max_length - input_ids.shape[1]
+
+ # Handle block configuration
+ if block_length is None:
+ block_length = gen_length # Default: single block (original behavior)
+
+ assert gen_length % block_length == 0, f"gen_length ({gen_length}) must be divisible by block_length ({block_length})"
+ num_blocks = gen_length // block_length
+
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (1 if i < rem else 0) for i in range(num_blocks)]
+ timesteps = [
+ torch.linspace(1, generation_config.eps, spb + 1, device=x.device)
+ for spb in steps_per_block
+ ]
+
+ if attention_mask is not None and torch.any(attention_mask == 0.0):
+ # we do not mask the [MASK] tokens so value = 1.0
+ attention_mask = F.pad(attention_mask, (0, max_length - attention_mask.shape[1]), value=1.0)
+ tok_idx = attention_mask.long().cumsum(-1) - 1
+ tok_idx.masked_fill_(attention_mask == 0, 1)
+ # attention_mask is of shape [B, N]
+ # broadcast to [B, 1, N, N]
+ attention_mask = torch.logical_and(
+ attention_mask.unsqueeze(1).unsqueeze(-2),
+ attention_mask.unsqueeze(1).unsqueeze(-1),
+ )
+ attention_mask = torch.where(attention_mask, torch.tensor(0.0, device=attention_mask.device), torch.tensor(float("-inf"), device=attention_mask.device))
+ else:
+ tok_idx = None
+ attention_mask = "full"
+
+
+
+ # Initialize cache for the prompt
+ past_key_values = None
+
+ # Process each block
+ for num_block in range(num_blocks):
+
+ current_block_start = input_ids.shape[1] + num_block * block_length
+ current_block_end = current_block_start + block_length
+
+ if cgws is not None:
+ window_end = max_length if cgws is None else min(current_block_end + cgws, max_length)
+ window_slice = slice(current_block_start, window_end)
+
+ # update cache
+ if use_cache:
+ model_output = model(x, attention_mask, tok_idx, use_cache=True)
+ past_key_values = model_output.past_key_values
+ # Extract only previous block cache
+ new_past_key_values = []
+ for i in range(len(past_key_values)):
+ new_past_key_values.append(())
+ for j in range(len(past_key_values[i])):
+ new_past_key_values[i] += (past_key_values[i][j][:, :current_block_start, :],)
+ past_key_values = new_past_key_values
+
+ else:
+ model_output = model(x, attention_mask, tok_idx, use_cache=False)
+
+ logits = model_output.logits
+ logits = torch.cat([logits[:,:1], logits[:, :-1]], dim=1)
+ _, x0 = sample_tokens(logits, temperature=temperature, top_p=top_p, top_k=top_k)
+ x[:, current_block_start] = x0[:, current_block_start]
+ if histories is not None:
+ histories.append(x.clone().cpu())
+
+
+
+ spb = steps_per_block[num_block]
+ i = 1
+ while True:
+
+
+ if cgws is not None:
+ mask_index = (x[:, window_slice] == mask_token_id)
+ else:
+ mask_index = (x[:, current_block_start:] == mask_token_id)
+
+
+ # Prepare attention mask for cached generation
+ if attention_mask != "full":
+ # Adjust attention mask for current position
+ if cgws is not None:
+ current_attention_mask = attention_mask[:, :, window_slice, :window_end]
+ else:
+ current_attention_mask = attention_mask[:, :, current_block_start:, :]
+ else:
+ current_attention_mask = attention_mask
+
+ if use_cache:
+ if cgws is not None:
+ model_output = model(x[:, window_slice], current_attention_mask,
+ tok_idx[:, window_slice] if tok_idx is not None else None,
+ past_key_values=past_key_values, use_cache=True)
+ else:
+ model_output = model(x[:, current_block_start:], current_attention_mask,
+ tok_idx[:, current_block_start:] if tok_idx is not None else None,
+ past_key_values=past_key_values, use_cache=True)
+ logits = model_output.logits
+ logits = torch.cat([logits[:,:1], logits[:, :-1]], dim=1)
+ else:
+ model_output = model(x, attention_mask, tok_idx, use_cache=False)
+ logits = model_output.logits
+ logits = logits[:, current_block_start:]
+ logits = torch.cat([logits[:,:1], logits[:, :-1]], dim=1)
+
+ if (x[:, current_block_start:current_block_end] == mask_token_id).sum() == 0:
+ break
+
+
+ mask_index[:, block_length:] = False
+ mask_logits = logits[mask_index]
+ target, x0 = sample_tokens(mask_logits, temperature, top_p=top_p, top_k=top_k, tar=tar)
+
+ # —— pad token penalty ——
+ _pad_target_divisor = pad_target_penalty
+ _pad_mask_flat = (x0 == pad_token_id)
+ if _pad_mask_flat.any():
+ target = target.clone()
+ target[_pad_mask_flat] = target[_pad_mask_flat] / _pad_target_divisor
+
+ if cgws is not None:
+ full_target = torch.full_like(x[:, window_slice], -torch.inf, device=model.device, dtype=logits.dtype)
+ else:
+ full_target = torch.full_like(x[:, current_block_start:], -torch.inf, device=model.device, dtype=logits.dtype)
+ full_target = full_target.float()
+ full_target[mask_index] = target
+ full_target[:, block_length:] = -torch.inf
+
+ if unmask_threshold is None:
+
+ num_mask_token = mask_index.sum() / mask_index.shape[0]
+ t = timesteps[num_block][i]
+ s = timesteps[num_block][i + 1]
+ number_transfer_tokens = int(num_mask_token * (1 - s / t)) if i < spb - 1 else int(num_mask_token)
+
+ if number_transfer_tokens > 0:
+ if alg_temp is None or alg_temp == 0:
+ _, transfer_index = torch.topk(full_target, number_transfer_tokens)
+ else:
+ full_target = full_target / alg_temp
+ full_target = F.softmax(full_target, dim=-1)
+ transfer_index = torch.multinomial(full_target, num_samples=number_transfer_tokens)
+
+ if cgws is not None:
+ x_ = torch.zeros_like(x[:, window_slice], device=model.device, dtype=torch.long) + mask_token_id
+ else:
+ x_ = torch.zeros_like(x[:, current_block_start:], device=model.device, dtype=torch.long) + mask_token_id
+ x_[mask_index] = x0.clone()
+ row_indices = torch.arange(x.size(0), device=model.device).unsqueeze(1).expand_as(transfer_index)
+
+
+
+ if cgws is not None:
+ x[:, window_slice][row_indices,transfer_index] = x_[row_indices,transfer_index]
+ else:
+ x[:, current_block_start:][row_indices,transfer_index] = x_[row_indices,transfer_index]
+
+
+
+ else:
+ if cgws is not None:
+ xwin = x[:, window_slice]
+ else:
+ xwin = x[:, current_block_start:]
+
+ selected_map = torch.zeros_like(xwin, dtype=torch.bool)
+ selected_map[mask_index] = (target >= unmask_threshold)
+ no_sel = ~selected_map.any(dim=-1) # [B]
+ no_sel = no_sel & mask_index.any(dim=-1)
+
+ if no_sel.any():
+ masked_scores = full_target.masked_fill(~mask_index, float("-inf"))
+ best_idx = torch.argmax(masked_scores, dim=-1)
+ selected_rows = torch.nonzero(no_sel, as_tuple=False).squeeze(-1)
+ selected_map[selected_rows, best_idx[selected_rows]] = True
+
+ selected_map &= mask_index
+ x_candidates = torch.full_like(xwin, mask_token_id, dtype=torch.long)
+ x_candidates[mask_index] = x0
+
+ xwin[selected_map] = x_candidates[selected_map]
+
+
+
+ if histories is not None:
+ histories.append(x.clone().cpu())
+
+ i += 1
+
+ if (x[:, current_block_start:current_block_end] == mask_token_id).sum() == 0:
+ break
+
+ block_all_pad = torch.all(
+ x[:, current_block_start:current_block_end] == pad_token_id
+ )
+ if block_all_pad:
+ if current_block_end < x.size(1):
+ x[:, current_block_end:] = pad_token_id
+ if histories is not None:
+ histories.append(x.clone().cpu())
+ break
+
+
+ if return_dict_in_generate:
+ return DreamModelOutput(
+ sequences=x,
+ history=histories,
+ )
+ else:
+ return x
+
+
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+
+ for t, snap in enumerate(history, start=0):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ if (step_map == 0).sum() == 0:
+ break
+ return step_map
+
+
+
+from tqdm import tqdm
+
+
+
+
+def worker(pretrained_model, rank, prompts, orig_idx, seq_dict, step_dict, batch_size, config):
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+
+ # load model once
+ model_gpu = (DreamModel.from_pretrained(pretrained_model,
+ trust_remote_code=True,
+ torch_dtype=torch.bfloat16)
+ .to(device)
+ .eval())
+ model_gpu.diffusion_generate = types.MethodType(DreamGenerationMixin.diffusion_generate, model_gpu)
+ model_gpu._sample = types.MethodType(DreamGenerationMixin._sample, model_gpu)
+ tokenizer_gpu = DreamTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+ pad_id = model_gpu.config.pad_token_id
+ mask_id = model_gpu.config.mask_token_id
+ eos_id = tokenizer_gpu.convert_tokens_to_ids("<|im_end|>")
+
+ # process in chunks of `batch_size`
+ for start in tqdm(range(0, len(prompts), batch_size),
+ desc=f"GPU {rank}", position=rank, leave=True):
+ batch_prompts = prompts[start:start+batch_size]
+ batch_idxs = orig_idx[start:start+batch_size]
+
+ # tokenize & move to GPU
+ enc = tokenizer_gpu(batch_prompts,
+ padding=True, #truncation=True,
+ return_tensors="pt", padding_side="left")
+ prompt_ids = enc["input_ids"].to(device)
+
+ attn_mask = prompt_ids.ne(pad_id)
+ #attn_mask = torch.ones_like(prompt_ids, dtype=torch.bool)
+ attn_mask = attn_mask.to(device=model_gpu.device)
+
+ if config.rollout.use_cache == False:
+ config.rollout.further_horizon = None
+
+ generation_config = DreamGenerationConfig(
+ output_history=True,
+ return_dict_in_generate=True,
+ max_length=config.rollout.max_gen_length + prompt_ids.shape[1],
+ steps=config.rollout.steps,
+ temperature=config.rollout.temperature,
+ top_p=config.rollout.top_p,
+ top_k=config.rollout.top_k,
+ tar=config.rollout.target,
+ alg_temp=config.rollout.alg_temp,
+ )
+
+ if config.rollout.remasking_strategy == "low_confidence_static":
+ unmask_threshold = None
+ else:
+ unmask_threshold = config.rollout.dynamic_threshold
+
+ generation_ids = _sample(
+ model_gpu,
+ prompt_ids,
+ attention_mask=attn_mask,
+ generation_config=generation_config,
+ block_length=config.rollout.block_size,
+ use_cache=config.rollout.use_cache,
+ further_horizon=config.rollout.further_horizon,
+ mask_token_id = mask_id,
+ eos_token_id = eos_id,
+ pad_token_id = pad_id,
+ pad_target_penalty = config.rollout.pad_target_penalty,
+ unmask_threshold = unmask_threshold
+ )
+ generation_ids.sequences = generation_ids.sequences.cpu()
+ torch.cuda.empty_cache()
+
+ # decode
+ seq_ids = generation_ids.sequences[:, prompt_ids.shape[1]:].tolist()
+ texts = tokenizer_gpu.batch_decode(
+ seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)
+
+ # compute and store step maps
+ for i, idx in enumerate(batch_idxs):
+ # extract step map for sample i in this batch
+ m = denoise_step_map(generation_ids.history, mask_id=mask_id, sample_idx=i)
+ step_map = m[prompt_ids.shape[1]:].tolist()
+ seq_dict[idx] = texts[i]
+ step_dict[idx] = step_map
+
+ # free unused GPU cache
+ torch.cuda.empty_cache()
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+ mp.set_start_method("spawn", force=True)
+
+
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+ system_prompts = '''<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+
+ project_name = config.experiment.project
+
+ code_eval = False
+
+ dataset = config.dataset.eval_dataset
+ pretrained_model = config.model
+ if config.dataset.data_type == "code":
+ code_eval = True
+ system_prompts_function = '''<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ elif config.dataset.data_type == "option":
+ system_prompts = '''<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\nThis is the problem:\n{{problem}}\nYou need to think step by step and put the final option (A, B, C, or D only—no other character) in \\boxed{}. <|im_end|>\n<|im_start|>assistant\n'''
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(32)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ data = get_data_chunk(data, num_node, node_index)
+
+ num = len(data)
+
+ tokenizer = DreamTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_eval:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+ # --------------------- 2. split to each GPU ----------------------
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1, "need >=2 GPUs for parallel inference"
+
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+
+
+
+ # ------------------- 4. launch all workers -----------------------
+ manager = mp.Manager()
+ seq_dict = manager.dict() # {shuffled_pos: text}
+ step_dict = manager.dict() # {shuffled_pos: step_map}
+ procs = []
+
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ seq_dict,
+ step_dict,
+ batch_size,
+ config))
+ p.start()
+ procs.append(p)
+
+ for p in procs:
+ p.join()
+
+ # ------------------- 5. restore original order -------------------
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+ import re
+
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_eval:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ else:
+ extracted_output = extract_code(full_output)
+ else:
+ extracted_output = extract_final_boxed_answer(full_output)
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+
diff --git a/sample/get_trace_viewer.py b/sample/get_trace_viewer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3fc129a8c4cda70ed8b00ef737421a3f1371084
--- /dev/null
+++ b/sample/get_trace_viewer.py
@@ -0,0 +1,279 @@
+import os
+from jetengine_ext.llm import LLM
+from jetengine_ext.sampling_params import SamplingParams
+from transformers import AutoTokenizer
+
+model_path = "/abs/path/of/your/model"
+tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
+# Initialize the LLM
+llm = LLM(
+ model_path,
+ enforce_eager=True,
+ tensor_parallel_size=1,
+ mask_token_id=151669,
+ block_length=4
+)
+
+# Set sampling/generation parameters
+sampling_params = SamplingParams(
+ temperature=1.0,
+ topk=0,
+ topp=1.0,
+ max_tokens=256,
+ remasking_strategy="low_confidence_dynamic",
+ block_length=4,
+ denoising_steps=4,
+ dynamic_threshold=0.9
+)
+
+# Prepare a simple chat-style prompt
+prompt = tokenizer.apply_chat_template(
+ [{"role": "user", "content": "Explain what reinforcement learning is in simple terms."}],
+ tokenize=False,
+ add_generation_prompt=True
+)
+
+# Generate text
+outputs = llm.generate_streaming([prompt], sampling_params)
+
+output_text = outputs[0]["text"]
+step_map = outputs[0]["first_unmask_times"]
+token_ids = outputs[0]["token_ids"]
+
+#print(output_text)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### output trace viewer
+pieces = []
+prev = ""
+for i in range(len(token_ids)):
+ cur = tokenizer.decode(token_ids[:i+1], skip_special_tokens=False, clean_up_tokenization_spaces=False)
+ piece = cur[len(prev):]
+ pieces.append(piece)
+ prev = cur
+
+# 2) 标记特殊 token(可在前端开关隐藏)
+try:
+ special_ids = set(getattr(tokenizer, "all_special_ids", []) or [])
+except Exception:
+ special_ids = set()
+is_special = [tid in special_ids for tid in token_ids]
+
+# 3) 写出自包含 HTML 文件
+import json, os
+
+data = {
+ "pieces": pieces,
+ "step_map": step_map,
+ "is_special": is_special,
+}
+
+# --- build data for HTML ---
+step_map = outputs[0].get("first_unmask_time") or outputs[0].get("first_unmask_times")
+token_ids = outputs[0]["token_ids"]
+
+# 逐 token 的增量片段,确保和 tokenizer 解码一致
+pieces = []
+prev = ""
+for i in range(len(token_ids)):
+ cur = tokenizer.decode(token_ids[:i+1], skip_special_tokens=False, clean_up_tokenization_spaces=False)
+ piece = cur[len(prev):]
+ pieces.append(piece)
+ prev = cur
+
+# 标记特殊 token(用于可选隐藏)
+try:
+ special_ids = set(getattr(tokenizer, "all_special_ids", []) or [])
+except Exception:
+ special_ids = set()
+is_special = [tid in special_ids for tid in token_ids]
+
+import json, os
+
+data = {
+ "pieces": pieces,
+ "step_map": step_map,
+ "is_special": is_special,
+}
+
+# --- self-contained HTML (fixed-width || for masked tokens) ---
+html = f"""
+
+
+
+Trace Viewer
+
+
+
+
+ Unmask Viewer
+
+
+
+
+
+
+
+"""
+
+out_path = "trace_viewer.html"
+with open(out_path, "w", encoding="utf-8") as f:
+ f.write(html)
+
+print(f"[Trace Viewer] Wrote: {os.path.abspath(out_path)} (open in your browser)")
+
+
+
diff --git a/sample/jetengine_ext/__init__.py b/sample/jetengine_ext/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4725d2d06f87d0ffa8c6a6d9344b2180ddbc7836
--- /dev/null
+++ b/sample/jetengine_ext/__init__.py
@@ -0,0 +1,8 @@
+from jetengine_ext.llm import LLM
+from jetengine_ext.sampling_params import SamplingParams
+
+
+__all__ = [
+ "LLM",
+ "SamplingParams",
+]
diff --git a/sample/jetengine_ext/config.py b/sample/jetengine_ext/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..916b46773539ef2ffe45e6b3640e99fedb9d5fe2
--- /dev/null
+++ b/sample/jetengine_ext/config.py
@@ -0,0 +1,29 @@
+import os
+from dataclasses import dataclass
+from transformers import AutoConfig
+
+
+@dataclass
+class Config:
+ model: str
+ max_num_batched_tokens: int = 16384
+ max_num_seqs: int = 512
+ max_model_len: int = 4096
+ gpu_memory_utilization: float = 0.5
+ tensor_parallel_size: int = 1
+ enforce_eager: bool = False
+ hf_config: AutoConfig | None = None
+ eos: int = -1
+ kvcache_block_size: int = 256
+ num_kvcache_blocks: int = -1
+ mask_token_id: int = -1
+ block_length: int = 4
+
+ def __post_init__(self):
+ assert os.path.isdir(self.model)
+ assert self.kvcache_block_size % 256 == 0
+ assert 1 <= self.tensor_parallel_size <= 8
+ self.hf_config = AutoConfig.from_pretrained(self.model, trust_remote_code=True)
+ self.max_model_len = min(self.max_model_len, self.hf_config.max_position_embeddings)
+ assert self.max_num_batched_tokens >= self.max_model_len
+ assert self.mask_token_id != -1, "Mask token ID must be set"
diff --git a/sample/jetengine_ext/layers/activation.py b/sample/jetengine_ext/layers/activation.py
new file mode 100644
index 0000000000000000000000000000000000000000..83e4734a80570c5daef733dfffdc3dcca8abc3dc
--- /dev/null
+++ b/sample/jetengine_ext/layers/activation.py
@@ -0,0 +1,15 @@
+import torch
+from torch import nn
+import torch.nn.functional as F
+from liger_kernel.ops.swiglu import LigerSiLUMulFunction
+
+
+class SiluAndMul(nn.Module):
+
+ def __init__(self):
+ super().__init__()
+
+ @torch.compile
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ x, y = x.chunk(2, -1)
+ return LigerSiLUMulFunction.apply(x, y)
diff --git a/sample/jetengine_ext/layers/attention.py b/sample/jetengine_ext/layers/attention.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9ffae6c906e7cbb1251bc13041c4032062d41c4
--- /dev/null
+++ b/sample/jetengine_ext/layers/attention.py
@@ -0,0 +1,103 @@
+import torch
+from torch import nn
+import triton
+import triton.language as tl
+
+from jetengine_ext.utils.context import get_context
+from jetengine_ext.engine.sequence import RunType
+from jetengine_ext.kernels.triton.attention import sparse_attn_varlen
+# from jetengine.kernels.triton.attention import fused_kv_cache_attention
+# from jetengine.kernels.triton.attention import fused_kv_cache_attention_v5
+from flash_attn import flash_attn_with_kvcache
+
+
+@triton.jit
+def store_kvcache_kernel(
+ key_ptr,
+ key_stride,
+ value_ptr,
+ value_stride,
+ k_cache_ptr,
+ v_cache_ptr,
+ slot_mapping_ptr,
+ D: tl.constexpr,
+):
+ idx = tl.program_id(0)
+ key_offsets = idx * key_stride + tl.arange(0, D)
+ value_offsets = idx * value_stride + tl.arange(0, D)
+ key = tl.load(key_ptr + key_offsets)
+ value = tl.load(value_ptr + value_offsets)
+ slot = tl.load(slot_mapping_ptr + idx)
+ cache_offsets = slot * D + tl.arange(0, D)
+ tl.store(k_cache_ptr + cache_offsets, key)
+ tl.store(v_cache_ptr + cache_offsets, value)
+
+
+def store_kvcache(key: torch.Tensor, value: torch.Tensor, k_cache: torch.Tensor, v_cache: torch.Tensor, slot_mapping: torch.Tensor):
+ N, num_heads, head_dim = key.shape
+ D = num_heads * head_dim
+ assert key.stride(-1) == 1 and value.stride(-1) == 1
+ assert key.stride(1) == head_dim and value.stride(1) == head_dim
+ assert k_cache.stride(1) == D and v_cache.stride(1) == D
+ assert slot_mapping.numel() == N
+ store_kvcache_kernel[(N,)](key, key.stride(0), value, value.stride(0), k_cache, v_cache, slot_mapping, D)
+
+
+class Attention(nn.Module):
+
+ def __init__(
+ self,
+ num_heads,
+ head_dim,
+ scale,
+ num_kv_heads,
+ ):
+ super().__init__()
+ self.num_heads = num_heads
+ self.head_dim = head_dim
+ self.scale = scale
+ self.num_kv_heads = num_kv_heads
+ self.k_cache = self.v_cache = torch.tensor([])
+
+ def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor):
+ pass
+
+class BlockAttention(Attention):
+ def __init__(
+ self,
+ num_heads,
+ head_dim,
+ scale,
+ num_kv_heads,
+ ):
+ super().__init__(num_heads, head_dim, scale, num_kv_heads)
+
+ def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor):
+ o: torch.Tensor
+ q = q.view(-1, self.num_heads, self.head_dim)
+ k = k.view(-1, self.num_kv_heads, self.head_dim)
+ v = v.view(-1, self.num_kv_heads, self.head_dim)
+ context = get_context()
+ k_cache, v_cache = self.k_cache, self.v_cache
+
+ should_store_whole = (context.run_type == RunType.PREFILL)
+ if should_store_whole and k_cache.numel() and v_cache.numel():
+ store_kvcache(k, v, k_cache, v_cache, context.slot_mapping)
+
+ if context.run_type == RunType.PREFILL:
+ o = sparse_attn_varlen(q, k, v,
+ cu_seqlens_q=context.cu_seqlens_q,
+ cu_seqlens_k=context.cu_seqlens_k,
+ staircase_size=context.block_length)
+ else:
+ q = q.view(-1, context.block_length, self.num_heads, self.head_dim)
+ k = k.view(-1, context.block_length, self.num_kv_heads, self.head_dim)
+ v = v.view(-1, context.block_length, self.num_kv_heads, self.head_dim)
+ o = flash_attn_with_kvcache(q, k_cache=k_cache, v_cache=v_cache, k=k, v=v,
+ cache_seqlens=context.context_lens,
+ block_table=context.block_tables,
+ causal=False) # Assuming non-causal for benchmark consistency
+ o = o.view(-1, self.num_heads * self.head_dim)
+ return o
+
+
diff --git a/sample/jetengine_ext/layers/embed_head.py b/sample/jetengine_ext/layers/embed_head.py
new file mode 100644
index 0000000000000000000000000000000000000000..96968a29911e032c59932803792439840872f66b
--- /dev/null
+++ b/sample/jetengine_ext/layers/embed_head.py
@@ -0,0 +1,72 @@
+import torch
+from torch import nn
+import torch.nn.functional as F
+import torch.distributed as dist
+
+from jetengine_ext.engine.sequence import RunType
+from jetengine_ext.utils.context import get_context
+
+
+class VocabParallelEmbedding(nn.Module):
+
+ def __init__(
+ self,
+ num_embeddings: int,
+ embedding_dim: int,
+ ):
+ super().__init__()
+ self.tp_rank = dist.get_rank()
+ self.tp_size = dist.get_world_size()
+ assert num_embeddings % self.tp_size == 0
+ self.num_embeddings = num_embeddings
+ self.num_embeddings_per_partition = self.num_embeddings // self.tp_size
+ self.vocab_start_idx = self.num_embeddings_per_partition * self.tp_rank
+ self.vocab_end_idx = self.vocab_start_idx + self.num_embeddings_per_partition
+ self.weight = nn.Parameter(torch.empty(self.num_embeddings_per_partition, embedding_dim))
+ self.weight.weight_loader = self.weight_loader
+
+ def weight_loader(self, param: nn.Parameter, loaded_weight: torch.Tensor):
+ param_data = param.data
+ shard_size = param_data.size(0)
+ start_idx = self.tp_rank * shard_size
+ loaded_weight = loaded_weight.narrow(0, start_idx, shard_size)
+ assert param_data.size() == loaded_weight.size()
+ param_data.copy_(loaded_weight)
+
+ def forward(self, x: torch.Tensor):
+ if self.tp_size > 1:
+ mask = (x >= self.vocab_start_idx) & (x < self.vocab_end_idx)
+ x = mask * (x - self.vocab_start_idx)
+ y = F.embedding(x, self.weight)
+ if self.tp_size > 1:
+ y = mask.unsqueeze(1) * y
+ dist.all_reduce(y)
+ return y
+
+
+class ParallelLMHead(VocabParallelEmbedding):
+
+ def __init__(
+ self,
+ num_embeddings: int,
+ embedding_dim: int,
+ bias: bool = False,
+ ):
+ super().__init__(num_embeddings, embedding_dim)
+ if bias:
+ self.bias = nn.Parameter(torch.empty(self.num_embeddings_per_partition))
+ self.bias.weight_loader = self.weight_loader
+ else:
+ self.register_parameter("bias", None)
+
+ def forward(self, x: torch.Tensor):
+ context = get_context()
+ if context.run_type == RunType.PREFILL:
+ last_indices = context.cu_seqlens_q[1:] - 1
+ x = x[last_indices].contiguous()
+ logits = F.linear(x, self.weight, self.bias)
+ if self.tp_size > 1:
+ all_logits = [torch.empty_like(logits) for _ in range(self.tp_size)] if self.tp_rank == 0 else None
+ dist.gather(logits, all_logits, 0)
+ logits = torch.cat(all_logits, -1) if self.tp_rank == 0 else None
+ return logits
diff --git a/sample/jetengine_ext/layers/linear.py b/sample/jetengine_ext/layers/linear.py
new file mode 100644
index 0000000000000000000000000000000000000000..c625a9ebd0be699b869184ede34691d640367351
--- /dev/null
+++ b/sample/jetengine_ext/layers/linear.py
@@ -0,0 +1,174 @@
+import torch
+from torch import nn
+import torch.nn.functional as F
+import torch.distributed as dist
+
+
+def divide(numerator, denominator):
+ assert numerator % denominator == 0
+ return numerator // denominator
+
+
+class LinearBase(nn.Module):
+
+ def __init__(
+ self,
+ input_size: int,
+ output_size: int,
+ tp_dim: int | None = None,
+ ):
+ super().__init__()
+ self.input_size = input_size
+ self.output_size = output_size
+ self.tp_dim = tp_dim
+ self.tp_rank = dist.get_rank()
+ self.tp_size = dist.get_world_size()
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ raise NotImplementedError
+
+
+class ReplicatedLinear(LinearBase):
+
+ def __init__(
+ self,
+ input_size: int,
+ output_size: int,
+ bias: bool = False,
+ ):
+ super().__init__(input_size, output_size)
+ self.weight = nn.Parameter(torch.empty(self.output_size, self.input_size))
+ self.weight.weight_loader = self.weight_loader
+ if bias:
+ self.bias = nn.Parameter(torch.empty(self.output_size))
+ self.bias.weight_loader = self.weight_loader
+ else:
+ self.register_parameter("bias", None)
+
+ def weight_loader(self, param: nn.Parameter, loaded_weight: torch.Tensor):
+ param.data.copy_(loaded_weight)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return F.linear(x, self.weight, self.bias)
+
+
+class ColumnParallelLinear(LinearBase):
+
+ def __init__(
+ self,
+ input_size: int,
+ output_size: int,
+ bias: bool = False,
+ ):
+ super().__init__(input_size, output_size, 0)
+ self.input_size_per_partition = input_size
+ self.output_size_per_partition = divide(output_size, self.tp_size)
+
+ self.weight = nn.Parameter(torch.empty(self.output_size_per_partition, self.input_size))
+ self.weight.weight_loader = self.weight_loader
+ if bias:
+ self.bias = nn.Parameter(torch.empty(self.output_size_per_partition))
+ self.bias.weight_loader = self.weight_loader
+ else:
+ self.register_parameter("bias", None)
+
+ def weight_loader(self, param: nn.Parameter, loaded_weight: torch.Tensor):
+ param_data = param.data
+ shard_size = param_data.size(self.tp_dim)
+ start_idx = self.tp_rank * shard_size
+ loaded_weight = loaded_weight.narrow(self.tp_dim, start_idx, shard_size)
+ param_data.copy_(loaded_weight)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return F.linear(x, self.weight, self.bias)
+
+
+class MergedColumnParallelLinear(ColumnParallelLinear):
+
+ def __init__(
+ self,
+ input_size: int,
+ output_sizes: list[int],
+ bias: bool = False,
+ ):
+ self.output_sizes = output_sizes
+ super().__init__(input_size, sum(output_sizes), bias=bias)
+
+ def weight_loader(self, param: nn.Parameter, loaded_weight: torch.Tensor, loaded_shard_id: int):
+ param_data = param.data
+ shard_offset = sum(self.output_sizes[:loaded_shard_id]) // self.tp_size
+ shard_size = self.output_sizes[loaded_shard_id] // self.tp_size
+ param_data = param_data.narrow(self.tp_dim, shard_offset, shard_size)
+ loaded_weight = loaded_weight.chunk(self.tp_size, self.tp_dim)[self.tp_rank]
+ param_data.copy_(loaded_weight)
+
+
+class QKVParallelLinear(ColumnParallelLinear):
+
+ def __init__(
+ self,
+ hidden_size: int,
+ head_size: int,
+ total_num_heads: int,
+ total_num_kv_heads: int | None = None,
+ bias: bool = False,
+ ):
+ self.head_size = head_size
+ self.total_num_heads = total_num_heads
+ self.total_num_kv_heads = total_num_kv_heads or total_num_heads
+ tp_size = dist.get_world_size()
+ self.num_heads = divide(self.total_num_heads, tp_size)
+ self.num_kv_heads = divide(self.total_num_kv_heads, tp_size)
+ input_size = hidden_size
+ output_size = (self.total_num_heads + 2 * self.total_num_kv_heads) * self.head_size
+ super().__init__(input_size, output_size, bias)
+
+ def weight_loader(self, param: nn.Parameter, loaded_weight: torch.Tensor, loaded_shard_id: str):
+ param_data = param.data
+ assert loaded_shard_id in ["q", "k", "v"]
+ if loaded_shard_id == "q":
+ shard_size = self.num_heads * self.head_size
+ shard_offset = 0
+ elif loaded_shard_id == "k":
+ shard_size = self.num_kv_heads * self.head_size
+ shard_offset = self.num_heads * self.head_size
+ else:
+ shard_size = self.num_kv_heads * self.head_size
+ shard_offset = self.num_heads * self.head_size + self.num_kv_heads * self.head_size
+ param_data = param_data.narrow(self.tp_dim, shard_offset, shard_size)
+ loaded_weight = loaded_weight.chunk(self.tp_size, self.tp_dim)[self.tp_rank]
+ param_data.copy_(loaded_weight)
+
+
+class RowParallelLinear(LinearBase):
+
+ def __init__(
+ self,
+ input_size: int,
+ output_size: int,
+ bias: bool = False,
+ ):
+ super().__init__(input_size, output_size, 1)
+ self.input_size_per_partition = divide(input_size, self.tp_size)
+ self.output_size_per_partition = output_size
+
+ self.weight = nn.Parameter(torch.empty(self.output_size, self.input_size_per_partition))
+ self.weight.weight_loader = self.weight_loader
+ if bias:
+ self.bias = nn.Parameter(torch.empty(self.output_size))
+ self.bias.weight_loader = self.weight_loader
+ else:
+ self.register_parameter("bias", None)
+
+ def weight_loader(self, param: nn.Parameter, loaded_weight: torch.Tensor):
+ param_data = param.data
+ shard_size = param_data.size(self.tp_dim)
+ start_idx = self.tp_rank * shard_size
+ loaded_weight = loaded_weight.narrow(self.tp_dim, start_idx, shard_size)
+ param_data.copy_(loaded_weight)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ y = F.linear(x, self.weight, self.bias if self.tp_rank == 0 else None)
+ if self.tp_size > 1:
+ dist.all_reduce(y)
+ return y
diff --git a/sample/jetengine_ext/layers/rotary_embedding.py b/sample/jetengine_ext/layers/rotary_embedding.py
new file mode 100644
index 0000000000000000000000000000000000000000..c473420f560204ce2a49b208754c78cd6a518c0a
--- /dev/null
+++ b/sample/jetengine_ext/layers/rotary_embedding.py
@@ -0,0 +1,68 @@
+from functools import lru_cache
+import torch
+from torch import nn
+
+
+def apply_rotary_emb(
+ x: torch.Tensor,
+ cos: torch.Tensor,
+ sin: torch.Tensor,
+) -> torch.Tensor:
+ cos = cos.unsqueeze(-2)
+ sin = sin.unsqueeze(-2)
+ x1, x2 = torch.chunk(x.to(torch.float32), 2, dim=-1)
+ y1 = x1 * cos - x2 * sin
+ y2 = x2 * cos + x1 * sin
+ return torch.cat((y1, y2), dim=-1).to(x.dtype)
+
+
+class RotaryEmbedding(nn.Module):
+
+ def __init__(
+ self,
+ head_size: int,
+ rotary_dim: int,
+ max_position_embeddings: int,
+ base: float,
+ ) -> None:
+ super().__init__()
+ self.head_size = head_size
+ assert rotary_dim == head_size
+ inv_freq = 1.0 / (base**(torch.arange(0, rotary_dim, 2, dtype=torch.float) / rotary_dim))
+ t = torch.arange(max_position_embeddings, dtype=torch.float)
+ freqs = torch.einsum("i,j -> ij", t, inv_freq)
+ cos = freqs.cos()
+ sin = freqs.sin()
+ cache = torch.cat((cos, sin), dim=-1)
+ self.register_buffer("cos_sin_cache", cache, persistent=False)
+
+ @torch.compile
+ def forward(
+ self,
+ positions: torch.Tensor,
+ query: torch.Tensor,
+ key: torch.Tensor,
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ num_tokens = positions.size(0)
+ cos_sin = self.cos_sin_cache[positions]
+ cos, sin = cos_sin.chunk(2, dim=-1)
+ query_shape = query.shape
+ query = query.view(num_tokens, -1, self.head_size)
+ query = apply_rotary_emb(query, cos, sin).view(query_shape)
+ key_shape = key.shape
+ key = key.view(num_tokens, -1, self.head_size)
+ key = apply_rotary_emb(key, cos, sin).view(key_shape)
+ return query, key
+
+
+@lru_cache(1)
+def get_rope(
+ head_size: int,
+ rotary_dim: int,
+ max_position: int,
+ base: float,
+ rope_scaling: dict | None = None,
+):
+ assert rope_scaling is None
+ rotary_emb = RotaryEmbedding(head_size, rotary_dim, max_position, base)
+ return rotary_emb
diff --git a/sample/jetengine_ext/layers/sampler.py b/sample/jetengine_ext/layers/sampler.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4b56dc5385a36796551726adc11500bd1fc8262
--- /dev/null
+++ b/sample/jetengine_ext/layers/sampler.py
@@ -0,0 +1,41 @@
+import torch
+from torch.nn import functional as F
+
+
+def top_k_logits(logits, k):
+ if k <= 0:
+ return logits
+ else:
+ values, _ = torch.topk(logits, k)
+ min_values = values[..., -1, None]
+ return torch.where(logits < min_values, torch.full_like(logits, float('-inf')), logits)
+
+def top_p_logits(logits, p):
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
+ sorted_mask = cumulative_probs > p
+ sorted_mask[..., 1:] = sorted_mask[..., :-1].clone()
+ sorted_mask[..., 0] = False
+ mask_indices = torch.scatter(torch.full_like(logits, False, dtype=torch.bool),
+ -1, sorted_indices, sorted_mask)
+ logits = logits.masked_fill(mask_indices, float('-inf'))
+ return logits
+
+def sample_with_temperature_topk_topp(logits, temperature=1.0, top_k=0, top_p=1.0):
+ orig_shape = logits.shape[:-1] # [batch, block]
+ vocab_size = logits.shape[-1]
+
+ logits = logits.reshape(-1, vocab_size) # [batch*block, vocab]
+
+ if temperature != 1.0:
+ logits = logits / temperature
+ if top_k > 0:
+ logits = top_k_logits(logits, top_k)
+ if top_p < 1.0:
+ logits = top_p_logits(logits, top_p)
+ probs = F.softmax(logits, dim=-1) # shape: [batch*block, vocab]
+ assert probs.dim() == 2
+ token = torch.multinomial(probs, num_samples=1) # [batch*block, 1]
+ token_prob = torch.gather(probs, -1, token) # [batch*block, 1]
+
+ return token.view(*orig_shape), token_prob.view(*orig_shape)
diff --git a/sample/jetengine_ext/llm.py b/sample/jetengine_ext/llm.py
new file mode 100644
index 0000000000000000000000000000000000000000..a650f582a499ab0cec4dcfcbfe8edb8b1d47aea1
--- /dev/null
+++ b/sample/jetengine_ext/llm.py
@@ -0,0 +1,5 @@
+from jetengine_ext.engine.llm_engine import LLMEngine
+
+
+class LLM(LLMEngine):
+ pass
diff --git a/sample/jetengine_ext/sampling_params.py b/sample/jetengine_ext/sampling_params.py
new file mode 100644
index 0000000000000000000000000000000000000000..97b30b64fc69ad7231fac44c5470be9c0f790b55
--- /dev/null
+++ b/sample/jetengine_ext/sampling_params.py
@@ -0,0 +1,18 @@
+from dataclasses import dataclass
+from typing import Literal
+
+@dataclass
+class SamplingParams:
+ temperature: float = 1.0
+ max_tokens: int = 64
+ ignore_eos: bool = False
+
+ # Block Diffusion Parameters
+ block_length: int = 4
+ denoising_steps: int = 4
+ dynamic_threshold: float = 0.9
+ eb_threshold: float = 0.35
+ topk: int = 0
+ topp: float = 1
+ remasking_strategy: Literal['sequential', 'low_confidence_static', 'low_confidence_dynamic', 'entropy_bounded'] = 'low_confidence_static'
+ stop_words: list[int] | None = None
diff --git a/sample/lavida_rl_rollout_video_reversal.py b/sample/lavida_rl_rollout_video_reversal.py
new file mode 100644
index 0000000000000000000000000000000000000000..c084de6da3bec9f89adad35cba85e67ae56f0be3
--- /dev/null
+++ b/sample/lavida_rl_rollout_video_reversal.py
@@ -0,0 +1,676 @@
+from __future__ import annotations
+import os
+import sys
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+LAVIDA_ROOT = os.path.join(REPO_ROOT, "LaViDa")
+sys.path.insert(0, LAVIDA_ROOT)
+sys.path.insert(1, REPO_ROOT)
+import math, json, os, time, re
+from dataclasses import dataclass
+from typing import List, Tuple
+from collections import defaultdict
+import numpy as np
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+from transformers import AutoTokenizer
+from llava.model.builder import load_pretrained_model
+from llava.mm_utils import process_images, tokenizer_image_token, process_video_with_decord
+from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
+from llava.conversation import conv_templates
+import multiprocessing as mp
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+
+from transformers.utils import logging as hf_logging
+hf_logging.set_verbosity_error()
+
+import warnings
+warnings.filterwarnings("ignore", category=UserWarning)
+warnings.filterwarnings("ignore", category=FutureWarning)
+
+
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def add_gumbel_noise(logits, temperature):
+ if temperature == 0:
+ return logits
+ logits = logits.to(torch.float64)
+ noise = torch.rand_like(logits, dtype=torch.float64)
+ noise = (- torch.log(noise)) ** temperature
+ return logits.exp() / noise
+
+def get_num_transfer_tokens(mask_index, steps):
+ mask_num = mask_index.sum(dim=1, keepdim=True)
+ base = mask_num // steps
+ remainder = mask_num % steps
+ num_transfer_tokens = torch.zeros(mask_num.size(0), steps, device=mask_index.device, dtype=torch.int64) + base
+ for i in range(mask_num.size(0)):
+ num_transfer_tokens[i, :remainder[i]] += 1
+ return num_transfer_tokens
+
+@dataclass
+class DiffusionOutput:
+ sequences: torch.Tensor # final result (B, L_total) (GPU)
+ history: List[torch.Tensor] # all intermediate x (CPU)
+ nfe: int
+ L0: int # length of prompt
+
+def build_lavida_prompt(question: str, conv_template: str = "llada"):
+ import copy
+ conv = copy.deepcopy(conv_templates[conv_template])
+ conv.append_message(conv.roles[0], DEFAULT_IMAGE_TOKEN + "\n" + question)
+ conv.append_message(conv.roles[1], None)
+ return conv.get_prompt()
+
+@torch.no_grad()
+def generate_llada_v_with_history(
+ model, tokenizer, input_ids, images, image_sizes, modalities,
+ steps, gen_length, block_length, temperature,
+ target, unmask_threshold=None, mask_id=126336
+) -> DiffusionOutput:
+ position_ids = None
+ attention_mask = None
+ inputs_embeds = None
+
+ (_input_ids, position_ids, attention_mask, _pkv, inputs_embeds, _labels) = model.prepare_inputs_labels_for_multimodal(
+ input_ids, position_ids, attention_mask, None, None, images, modalities, image_sizes=image_sizes
+ )
+ assert input_ids.shape[1] < inputs_embeds.shape[1], "inputs_embeds must be longer than input_ids, because it contains image embeddings."
+ device = inputs_embeds.device
+ B = inputs_embeds.shape[0]
+ L0 = inputs_embeds.shape[1]
+
+ assert B == 1, "LLaDA-V only support batch size 1 for now"
+
+ total_len = L0 + gen_length
+ masked_embed = model.get_model().embed_tokens(torch.tensor([mask_id], device=device)) # (1, d)
+ x_embeds = masked_embed.repeat(B, total_len, 1)
+ x_embeds[:, :L0] = inputs_embeds
+ x_tokens = torch.full((B, total_len), mask_id, dtype=torch.long, device=device)
+ prompt_index = torch.zeros_like(x_tokens, dtype=torch.bool)
+ prompt_index[:, :L0] = True
+
+ assert gen_length % block_length == 0
+ num_blocks = gen_length // block_length
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (i < rem) for i in range(num_blocks)]
+
+ nfe = 0
+ hist: List[torch.Tensor] = []
+
+ for blk in range(num_blocks):
+ s = L0 + blk * block_length
+ e = L0 + (blk + 1) * block_length
+
+ cur_steps = steps_per_block[blk]
+
+ outputs = model.get_model()(
+ input_ids=None,
+ input_embeddings=x_embeds,
+ attention_mask=None,
+ use_cache=False,
+ )
+ logits = outputs.logits.float()
+
+ blk_tokens = x_tokens[:, s:e]
+ eos_mask = (blk_tokens == 126348)
+ if eos_mask.any():
+ first_eos_pos = torch.where(eos_mask[0])[0][0].item()
+ valid_region = torch.zeros_like(blk_tokens, dtype=torch.bool)
+ valid_region[:, :first_eos_pos] = True
+ else:
+ valid_region = torch.ones_like(blk_tokens, dtype=torch.bool)
+
+ mask_blk = (blk_tokens == mask_id) & valid_region
+ num_transfer = get_num_transfer_tokens(mask_blk, cur_steps)
+
+ x0_blk, tr_idx_blk = get_transfer_index(
+ logits[:, s:e], temperature, target,
+ mask_blk, blk_tokens, num_transfer[:, 0],
+ threshold=unmask_threshold
+ )
+ x_slice = x_tokens[:, s:e]
+ x_slice[tr_idx_blk] = x0_blk[tr_idx_blk]
+ x_tokens[:, s:e] = x_slice
+ x_embeds[:, s:e] = model.get_model().embed_tokens(x_tokens[:, s:e])
+ hist.append(x_tokens.clone().cpu())
+ nfe += 1
+
+ i = 1
+ while i < cur_steps:
+ nfe += 1
+ outputs = model.get_model()(
+ input_ids=None,
+ input_embeddings=x_embeds,
+ attention_mask=None,
+ use_cache=False,
+ )
+ logits = outputs.logits.float()
+
+ blk_tokens = x_tokens[:, s:e]
+ eos_mask = (blk_tokens == 126348)
+ if eos_mask.any():
+ first_eos_pos = torch.where(eos_mask[0])[0][0].item()
+ valid_region = torch.zeros_like(blk_tokens, dtype=torch.bool)
+ valid_region[:, :first_eos_pos] = True
+ else:
+ valid_region = torch.ones_like(blk_tokens, dtype=torch.bool)
+
+ mask_blk = (x_tokens[:, s:] == mask_id)
+ mask_blk[:, block_length:] = False
+ mask_blk[:, :block_length] = mask_blk[:, :block_length] & valid_region
+
+ x0, tr_idx = get_transfer_index(
+ logits[:, s:], temperature, target,
+ mask_blk, x_tokens[:, s:], num_transfer[:, i],
+ threshold=unmask_threshold
+ )
+
+ if tr_idx.any():
+ x_slice = x_tokens[:, s:]
+ x_slice[tr_idx] = x0[tr_idx]
+ x_tokens[:, s:] = x_slice
+ x0_embeds = model.get_model().embed_tokens(torch.where(tr_idx, x0, x_slice))
+ x_embeds[:, s:][tr_idx] = x0_embeds[tr_idx]
+
+ hist.append(x_tokens.clone().cpu())
+
+ remaining_masks = (x_tokens[:, s:e] == mask_id) & valid_region
+ if remaining_masks.sum() == 0:
+ break
+ i += 1
+
+ if (x_tokens[:, s:e] == 126348).any():
+ break
+
+ return DiffusionOutput(sequences=x_tokens, history=hist, nfe=nfe, L0=L0)
+
+def get_transfer_index(logits, temperature, target, mask_index, x, num_transfer_tokens, threshold=None):
+ logits_with_noise = add_gumbel_noise(logits, temperature=temperature)
+ logits_with_noise[..., 126336] = -float('inf')
+ x0 = torch.argmax(logits_with_noise, dim=-1)
+
+ if target == 'confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = torch.squeeze(torch.gather(p, dim=-1, index=torch.unsqueeze(x0, -1)), -1)
+ elif target == 'margin_confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ top2 = torch.topk(p, 2, dim=-1).values
+ x0_p = top2[..., 0] - top2[..., 1]
+ elif target == 'neg_entropy':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = -torch.sum(p * torch.log(p + 1e-10), dim=-1)
+ elif target == 'random':
+ x0_p = torch.rand((x0.shape[0], x0.shape[1]), device=x0.device)
+ else:
+ raise NotImplementedError(target)
+
+ x0 = torch.where(mask_index, x0, x)
+
+ if threshold is not None:
+ selected = mask_index & (x0_p >= threshold)
+ for j in range(selected.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0: continue
+ num_selected = selected[j].sum().item()
+ if num_selected < k:
+ masked_scores = x0_p[j].masked_fill(~mask_index[j], float("-inf"))
+ _, top_indices = torch.topk(masked_scores, k=k)
+ selected[j, :] = False
+ selected[j, top_indices] = True
+ return x0, selected
+
+ confidence = x0_p.masked_fill(~mask_index, float("-inf"))
+ transfer_index = torch.zeros_like(x0, dtype=torch.bool, device=x0.device)
+ for j in range(confidence.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0: continue
+ _, sel = torch.topk(confidence[j], k=k)
+ transfer_index[j, sel] = True
+ return x0, transfer_index
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+def get_prompt(data_i):
+ sys_prompt = 'Carefully watch the video and pay attention to the cause and sequence of events, the detail and movement of objects, and the action and pose of persons. Based on your observations, select the best option that accurately addresses the question.\n'
+ question_prompt="\nOnly give the best option."
+ multiple_choice_prompt = " Please provide only the single option letter (e.g., A, B, C, D, etc.) within the tags."
+ return sys_prompt + 'Question: ' + data_i["prompt"] + 'Options: ' + str(data_i['options']) + question_prompt + multiple_choice_prompt
+
+def extract_option_answer(text: str) -> str:
+ text = str(text).strip()
+ if not text:
+ return ""
+
+ # 1. 剔除结束符,保持文本干净
+ clean_text = text.replace("<|eot_id|>", "").strip()
+
+ # 2. 按段落拆分,直接取最后一段(模型通常会在 \n\n 后给出最终答案)
+ paragraphs = clean_text.split('\n\n')
+ last_paragraph = paragraphs[-1].strip()
+
+ # 3. 在最后一段的开头提取目标字母
+ # 解释:^\s* 匹配开头可能的空格,([A-Z]) 抓取字母,(?:[.\s]|$) 确保字母后面跟着的是点号、空格或直接结束
+ match = re.match(r'^\s*([A-Z])(?:[.\s]|$)', last_paragraph)
+ if match:
+ return match.group(1)
+
+ # 4. 终极防漏兜底:如果模型没有分段,或者格式全乱了
+ # 找出文本中所有 "大写字母 + 点号/空格" 的组合,取最后一个
+ fallback_matches = re.findall(r'\b([A-Z])(?:[.\s])', clean_text)
+ if fallback_matches:
+ return fallback_matches[-1]
+
+ return "Can not extract the answer!"
+
+def extract_multiple_choice_answer_regex(s: str) -> str:
+ # (?i) 表示忽略大小写,(.*?) 是非贪婪匹配
+ # re.DOTALL 允许跨行匹配,以防模型在标签内换行
+ pattern = re.compile(r'(.*?)', re.IGNORECASE | re.DOTALL)
+
+ matches = pattern.findall(s)
+ if not matches:
+ # return "Can not extract the answer!"
+ return extract_option_answer(s)
+
+ # 获取最后一个匹配项(最终答案),并去除首尾空格
+ final_answer = matches[-1].strip()
+
+ if len(final_answer) > 1:
+ # 比如将 "Option A" 变成 "A"
+ # import re
+ letters = re.findall(r'[A-Z]', final_answer.upper())
+ final_answer = letters[0] if letters else final_answer
+
+ return final_answer if final_answer else "Can not extract the answer!"
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag)
+ if start == -1: return "Can not extract the answer!"
+ i = start + len(tag)
+ depth = 1
+ buf = []
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{': depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: break
+ buf.append(ch)
+ i += 1
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+ for t, snap in enumerate(history, start=1):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ unprocessed_mask = (step_map == 0)
+ if unprocessed_mask.any():
+ max_step = step_map.max()
+ step_map[unprocessed_mask] = max_step
+ return step_map
+
+from tqdm import tqdm
+
+# ===== 修改点2:传入 shuffled_flags 供 worker 处理打乱逻辑 =====
+def worker(pretrained_model, rank, prompts, orig_idx, data_idx, media_paths, shuffled_flags, seq_dict, step_dict, media_abs_dict, batch_size, config):
+ from PIL import Image
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+
+ tokenizer_gpu, model_gpu, image_processor, max_length = load_pretrained_model(
+ pretrained_model, None, "llava_llada", attn_implementation="sdpa", device_map=device
+ )
+ model_gpu.eval()
+ special_tokens = {
+ "additional_special_tokens": [DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN]
+ }
+ num_new = tokenizer_gpu.add_special_tokens(special_tokens)
+ if num_new > 0:
+ model_gpu.resize_token_embeddings(len(tokenizer_gpu))
+
+ # LaViDa official code path: no custom dLLM cache / hook imports here.
+
+ mask_id = 126336
+ unmask_threshold = None if config.rollout.remasking_strategy == "low_confidence_static" else config.rollout.dynamic_threshold
+
+ for j in tqdm(range(len(prompts)), desc=f"GPU {rank}", position=rank, leave=True):
+ did = data_idx[j]
+ prompt_text = prompts[j]
+ m_path = media_paths[j] if media_paths is not None else None
+ is_shuffled = shuffled_flags[j] # 获取当前样本是否需要打乱
+
+ media_tensor = None
+ media_sizes = None
+ modalities = []
+
+ if m_path is not None:
+ m_path = os.path.abspath(m_path)
+ media_abs_dict[did] = m_path
+
+ if m_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
+ modalities = ["video"]
+ if not hasattr(model_gpu.config, "video_fps"): setattr(model_gpu.config, "video_fps", 1)
+ if not hasattr(model_gpu.config, "frames_upbound"): setattr(model_gpu.config, "frames_upbound", 16)
+ if not hasattr(model_gpu.config, "force_sample"): setattr(model_gpu.config, "force_sample", True)
+
+ video_frames, video_time, frame_time, num_frames_to_sample = process_video_with_decord(m_path, model_gpu.config)
+
+ # ====== T-GRPO 视频帧乱序逻辑 ======
+ if is_shuffled and isinstance(video_frames, np.ndarray) and len(video_frames) > 0:
+ # 使用 fixed seed 保证重现,基于原样本 idx 和 rank
+ np.random.seed(config.training.seed + orig_idx[j] * 10)
+ shuffle_indices = np.random.permutation(len(video_frames))
+ video_frames = video_frames[shuffle_indices]
+ # ===================================
+
+ image_tensor = image_processor.preprocess(video_frames, return_tensors="pt")["pixel_values"]
+ media_tensor = [image_tensor.to(dtype=torch.float16, device=device)]
+
+ if isinstance(video_frames, np.ndarray) and len(video_frames) > 0:
+ media_sizes = [(video_frames.shape[2], video_frames.shape[1])]
+ else:
+ media_sizes = [(0, 0)]
+
+ if getattr(model_gpu.config, "add_time_instruction", False):
+ # 即使画面被打乱,时间指令依旧输入正确的原时间序列,让模型产生冲突,从而进行惩罚
+ time_instruction = f"The video lasts for {video_time:.2f} seconds, and {num_frames_to_sample} frames are uniformly sampled from it. These frames are located at {frame_time}. Please answer the following questions related to this video."
+ clean_prompt = prompt_text.replace(DEFAULT_IMAGE_TOKEN, "").strip()
+ prompt_text = f"{time_instruction}\n{clean_prompt}"
+
+ else:
+ modalities = ["image"]
+ image = Image.open(m_path).convert("RGB")
+ if not hasattr(model_gpu.config, "image_aspect_ratio") or getattr(model_gpu.config, "image_aspect_ratio") is None:
+ setattr(model_gpu.config, "image_aspect_ratio", "pad")
+ if not hasattr(model_gpu.config, "mm_patch_merge_type") or getattr(model_gpu.config, "mm_patch_merge_type") is None:
+ setattr(model_gpu.config, "mm_patch_merge_type", "flat")
+
+ image_tensor_list = process_images([image], image_processor, model_gpu.config)
+ media_tensor = [_im.to(dtype=torch.float16, device=device) for _im in image_tensor_list]
+ media_sizes = [image.size]
+
+ conv_prompt = build_lavida_prompt(prompt_text, conv_template="llada")
+ input_ids = tokenizer_image_token(conv_prompt, tokenizer_gpu, IMAGE_TOKEN_INDEX, return_tensors="pt").unsqueeze(0).to(device)
+
+ out = generate_llada_v_with_history(
+ model=model_gpu,
+ tokenizer=tokenizer_gpu,
+ input_ids=input_ids,
+ images=media_tensor,
+ image_sizes=media_sizes,
+ modalities=modalities,
+ steps=config.rollout.steps,
+ gen_length=config.rollout.max_gen_length,
+ block_length=config.rollout.block_size,
+ temperature=config.rollout.temperature,
+ target=config.rollout.target,
+ unmask_threshold=unmask_threshold,
+ mask_id=mask_id,
+ )
+
+ out.sequences = out.sequences.cpu()
+ L0 = model_gpu.prepare_inputs_labels_for_multimodal(input_ids, None, None, None, None, media_tensor, modalities,
+ image_sizes=media_sizes)[4].shape[1]
+ assert L0 == out.L0, "L0 must be equal to the length of prompt."
+ seq_ids = out.sequences[:, L0:].tolist()
+ text = tokenizer_gpu.batch_decode(seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)[0]
+
+ m = denoise_step_map(out.history, mask_id=mask_id, sample_idx=0)
+ step_map = m[L0:].tolist()
+
+ seq_dict[orig_idx[j]] = text
+ step_dict[orig_idx[j]] = step_map
+
+ torch.cuda.empty_cache()
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+ mp.set_start_method("spawn", force=True)
+
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+ project_name = config.experiment.project
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+ assert config.dataset.data_type != "code", "lavida video reversal rollout is not supported for code tasks."
+
+ code_task = False
+ if config.experiment.function == "train":
+ dataset = config.dataset.train_dataset
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+
+ elif config.experiment.function == "evaluation":
+ dataset = config.evaluation.eval_dataset
+ k_sample = config.evaluation.num_response_per_task
+ batch_size = config.evaluation.batch_size
+ config.rollout.steps = config.evaluation.steps
+ config.rollout.temperature = config.evaluation.temperature
+ config.rollout.target = config.evaluation.target
+ config.rollout.block_size = config.evaluation.block_size
+ config.rollout.use_cache = config.evaluation.use_cache
+ config.rollout.further_horizon = config.evaluation.further_horizon
+ config.rollout.remasking_strategy = config.evaluation.remasking_strategy
+ config.rollout.dynamic_threshold = config.evaluation.dynamic_threshold
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+
+ # ===== 修改点1:基于 ID 分组,并衍生 Ordered / Shuffled 数据变体 =====
+ with open("../data/" + dataset + ".json", 'r') as f:
+ raw_data = json.load(f)
+
+ # 按照 data_id 进行数据分组
+ grouped_data = defaultdict(list)
+ for item in raw_data:
+ grouped_data[item["data_id"]].append(item)
+
+ unique_ids = list(grouped_data.keys())
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+
+ if num_node > 1:
+ if config.experiment.function == "train":
+ random.shuffle(unique_ids)
+ unique_ids = get_data_chunk(unique_ids, num_node, node_index)
+
+ # 在 Training 阶段,我们需要选择 ID,然后再展开成变体
+ if config.experiment.function == "train":
+ # 你的配置里 num_task_per_step 通常指最后展开序列的总数量 (如 128)
+ # 因为我们 1 个 ID 会衍生出 4 个变体 (Forward顺, Forward乱, Backward顺, Backward乱)
+ # 所以选取的 ID 数量应该是 num_task_per_step // 4
+ num_ids_to_select = config.rollout.num_task_per_step // 4
+ num_ids_to_select = min(num_ids_to_select, len(unique_ids))
+ unique_ids = random_select(unique_ids, num_ids_to_select)
+
+ # 将选中的 ID 分组展开成包含 is_shuffled 的平铺列表
+ data = []
+ for d_id in unique_ids:
+ for item in grouped_data[d_id]:
+ # 添加 Ordered 变体
+ ord_item = item.copy()
+ ord_item["is_shuffled"] = False
+ data.append(ord_item)
+
+ # 添加 Shuffled 变体
+ shuf_item = item.copy()
+ shuf_item["is_shuffled"] = True
+ data.append(shuf_item)
+
+ num = len(data)
+ # =================================================================
+
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True, local_files_only=True)
+
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+ if config.model.model_base in ["llava_llada", "llada_v", "llada-v", "lavida"]:
+ media_paths = []
+ for i in range(num):
+ p = data[i].get("video_abs_path") or data[i].get("image")
+ media_paths += [p] * k_sample
+ else:
+ media_paths = None
+
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1, "need >=2 GPUs for parallel inference"
+
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+ data_idx_full = [index_list[i] for i in shuffled_idx]
+ data_idx_chunks = split_even(data_idx_full, n_gpu)
+
+ if media_paths is not None:
+ media_paths_full = [media_paths[i] for i in shuffled_idx]
+ media_chunks = split_even(media_paths_full, n_gpu)
+ else:
+ media_chunks = [None] * n_gpu
+
+ # 提取 shuffled 标志列表,传给 GPU Worker
+ shuffled_flags_full = [data[index_list[i]].get("is_shuffled", False) for i in shuffled_idx]
+ shuffled_flags_chunks = split_even(shuffled_flags_full, n_gpu)
+
+ manager = mp.Manager()
+ seq_dict = manager.dict()
+ step_dict = manager.dict()
+ media_abs_dict = manager.dict()
+ procs = []
+
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ data_idx_chunks[rk],
+ media_chunks[rk],
+ shuffled_flags_chunks[rk], # <--- 将打乱标志传进去
+ seq_dict,
+ step_dict,
+ media_abs_dict,
+ batch_size,
+ config))
+ p.start()
+ procs.append(p)
+
+ for p in procs:
+ p.join()
+
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+
+ if config.model.model_base in ["llava_llada", "llada_v", "llada-v", "lavida"]:
+ for di in range(num):
+ if di in media_abs_dict:
+ path = media_abs_dict[di]
+ if path.endswith(('.mp4', '.avi', '.mov')):
+ data[di]["video_abs_path"] = path
+ else:
+ data[di]["image_abs_path"] = path
+
+ cprint("generation job done!", "green")
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = "<|mdm_mask|>"
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+
+ i = 0
+ for full_output in restored_outputs:
+ if config.dataset.data_type == "video_reverse" and not config.reward.answer_must_in_box:
+ extracted_output = full_output.strip()
+ elif config.reward.answer_must_in_box:
+ # extracted_output = extract_option_answer(full_output)
+ extracted_output = extract_multiple_choice_answer_regex(full_output)
+ # extracted_output = extract_final_boxed_answer(full_output)
+ else:
+ if "" in full_output:
+ extracted_output = full_output.split("")[1]
+ else:
+ extracted_output = full_output
+
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
diff --git a/sample/llada_rl_rollout.py b/sample/llada_rl_rollout.py
new file mode 100644
index 0000000000000000000000000000000000000000..28e649e8fa5e122f89a9345d22a15052be3ae5fb
--- /dev/null
+++ b/sample/llada_rl_rollout.py
@@ -0,0 +1,553 @@
+from __future__ import annotations
+import math, json, os, time
+from dataclasses import dataclass
+from typing import List, Tuple
+import numpy as np
+from jinja2 import Template
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+from transformers import AutoTokenizer, AutoModel
+from llada.modeling_llada import LLaDAModelLM
+import multiprocessing as mp
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def add_gumbel_noise(logits, temperature):
+ if temperature == 0:
+ return logits
+ logits = logits.to(torch.float64)
+ noise = torch.rand_like(logits, dtype=torch.float64)
+ noise = (- torch.log(noise)) ** temperature
+ return logits.exp() / noise
+
+
+def get_num_transfer_tokens(mask_index, steps):
+ mask_num = mask_index.sum(dim=1, keepdim=True)
+
+ base = mask_num // steps
+ remainder = mask_num % steps
+
+ num_transfer_tokens = torch.zeros(mask_num.size(0), steps, device=mask_index.device, dtype=torch.int64) + base
+
+ for i in range(mask_num.size(0)):
+ num_transfer_tokens[i, :remainder[i]] += 1
+
+ return num_transfer_tokens
+
+
+
+# ──────────────────────────── return type ────────────────────────────────
+@dataclass
+class DiffusionOutput:
+ sequences: torch.Tensor # final result (B, L_total) (GPU)
+ history: List[torch.Tensor] # all intermediate x (CPU)
+ nfe: int
+
+
+
+
+
+
+@torch.no_grad()
+def generate_with_prefix_cache(
+ model, prompt,
+ steps, gen_length, block_length, temperature,
+ target, mask_id, further_horizon, use_cache, unmask_threshold
+ ) -> DiffusionOutput:
+
+ cgws = further_horizon
+ B, L0 = prompt.shape
+ x = torch.full((B, L0 + gen_length), mask_id, dtype=torch.long, device=prompt.device)
+ max_length = L0 + gen_length
+ x[:, :L0] = prompt
+ assert gen_length % block_length == 0
+ num_blocks = gen_length // block_length
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (i < rem) for i in range(num_blocks)]
+
+ nfe = 0
+ hist: List[torch.Tensor] = []
+
+ for blk in range(num_blocks):
+ s, e = L0 + blk * block_length, L0 + (blk + 1) * block_length
+
+ if cgws is not None:
+ window_end = max_length if cgws is None else min(e + cgws, max_length)
+ window_slice = slice(s, window_end)
+
+ cur_steps = steps_per_block[blk]
+ num_transfer = get_num_transfer_tokens((x[:, s:e] == mask_id), cur_steps)
+
+ # first full forward to build prefix cache
+ if use_cache:
+ out = model(x, use_cache=True)
+ pkv = out.past_key_values
+ # chop prefix out of past_kv to keep cache small
+ new_pkv = tuple(
+ tuple(t[:, :, :s] for t in layer) for layer in pkv
+ )
+ pkv = new_pkv
+ else:
+ out = model(x, use_cache=False)
+
+ mask_all = (x == mask_id)
+ mask_all[:, e:] = 0
+
+ x0, tr_idx = get_transfer_index(
+ out.logits, temperature, target, mask_all,
+ x, num_transfer[:, 0], unmask_threshold)
+ x[tr_idx] = x0[tr_idx]
+ hist.append(x.clone().cpu())
+ nfe += 1
+
+ i = 1
+ while True:
+ nfe += 1
+ if cgws is not None:
+ mask_blk = (x[:, window_slice] == mask_id)
+ else:
+ mask_blk = (x[:, s:] == mask_id)
+ mask_blk[:, block_length:] = 0
+
+ if use_cache:
+ if cgws is not None:
+ logits = model(x[:, window_slice], past_key_values=pkv, use_cache=True).logits
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, window_slice], num_transfer[:, i], unmask_threshold)
+ x[:, window_slice][tr_idx] = x0[tr_idx]
+ else:
+ logits = model(x[:, s:], past_key_values=pkv, use_cache=True).logits
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, s:], num_transfer[:, i], unmask_threshold)
+ x[:, s:][tr_idx] = x0[tr_idx]
+ else:
+ logits = model(x, use_cache=False).logits
+ logits = logits[:, s:]
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, s:], num_transfer[:, i], unmask_threshold)
+ x[:, s:][tr_idx] = x0[tr_idx]
+
+ hist.append(x.clone().cpu())
+
+ if (x[:, s:e] == mask_id).sum() == 0:
+ break
+ i += 1
+
+ return DiffusionOutput(sequences=x, history=hist, nfe=nfe)
+
+
+
+
+def get_transfer_index(logits, temperature, target, mask_index, x, num_transfer_tokens, threshold=None):
+ logits_with_noise = add_gumbel_noise(logits, temperature=temperature)
+ x0 = torch.argmax(logits_with_noise, dim=-1) # b, l
+
+ if target == 'confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = torch.squeeze(
+ torch.gather(p, dim=-1, index=torch.unsqueeze(x0, -1)), -1) # b, l
+ elif target == 'margin_confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ top2 = torch.topk(p, 2, dim=-1).values # (b, l, 2)
+ x0_p = top2[..., 0] - top2[..., 1] # Δ(top1, top2)
+ elif target == 'neg_entropy':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = -torch.sum(p * torch.log(p + 1e-10), dim=-1) # –entropy
+ elif target == 'random':
+ x0_p = torch.rand((x0.shape[0], x0.shape[1]), device=x0.device)
+ else:
+ raise NotImplementedError(target)
+
+ x0 = torch.where(mask_index, x0, x)
+
+ if threshold is not None:
+ selected = mask_index & (x0_p >= threshold) # (B, T)
+
+ has_mask = mask_index.any(dim=-1) # (B,)
+ none_sel = (~selected.any(dim=-1)) & has_mask # (B,)
+ if none_sel.any():
+ masked_scores = x0_p.masked_fill(~mask_index, float("-inf"))
+ best_idx = masked_scores.argmax(dim=-1) # (B,)
+ rows = torch.nonzero(none_sel, as_tuple=False).squeeze(-1)
+ selected[rows, best_idx[rows]] = True
+
+ return x0, selected
+
+ confidence = x0_p.masked_fill(~mask_index, float("-inf"))
+ transfer_index = torch.zeros_like(x0, dtype=torch.bool, device=x0.device)
+ for j in range(confidence.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+ _, sel = torch.topk(confidence[j], k=k)
+ transfer_index[j, sel] = True
+ return x0, transfer_index
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+
+ for t, snap in enumerate(history, start=0):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ if (step_map == 0).sum() == 0:
+ break
+ return step_map
+
+
+
+from tqdm import tqdm
+
+def worker(pretrained_model, rank, prompts, orig_idx, seq_dict, step_dict, batch_size, config):
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+
+ # load model once
+ model_gpu = (LLaDAModelLM
+ .from_pretrained(pretrained_model,
+ trust_remote_code=True,
+ torch_dtype=torch.bfloat16)
+ .to(device)
+ .eval())
+
+ tokenizer_gpu = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+ # process in chunks of `batch_size`
+ for start in tqdm(range(0, len(prompts), batch_size),
+ desc=f"GPU {rank}", position=rank, leave=True):
+ batch_prompts = prompts[start:start+batch_size]
+ batch_idxs = orig_idx[start:start+batch_size]
+
+ # tokenize & move to GPU
+ enc = tokenizer_gpu(batch_prompts,
+ padding=True, #truncation=True,
+ return_tensors="pt", padding_side="left")
+ input_ids = enc["input_ids"].to(device)
+
+ mask_id = tokenizer_gpu.encode('<|mdm_mask|>')[0]
+
+ if config.rollout.use_cache == False:
+ config.rollout.further_horizon = None
+
+ if config.rollout.remasking_strategy == "low_confidence_static":
+ unmask_threshold = None
+ else:
+ unmask_threshold = config.rollout.dynamic_threshold
+
+ # generate_with_prefix_cache
+ out = generate_with_prefix_cache(
+ model_gpu, input_ids,
+ steps=config.rollout.steps, gen_length=config.rollout.max_gen_length,
+ block_length=config.rollout.block_size, temperature=config.rollout.temperature,
+ target=config.rollout.target, mask_id=mask_id, further_horizon=config.rollout.further_horizon,
+ use_cache=config.rollout.use_cache, unmask_threshold = unmask_threshold
+ )
+ out.sequences = out.sequences.cpu()
+ torch.cuda.empty_cache()
+
+ # decode
+ seq_ids = out.sequences[:, input_ids.shape[1]:].tolist()
+ texts = tokenizer_gpu.batch_decode(
+ seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)
+
+
+
+ # compute and store step maps
+ for i, idx in enumerate(batch_idxs):
+ # extract step map for sample i in this batch
+ m = denoise_step_map(out.history, mask_id=mask_id, sample_idx=i)
+ step_map = m[input_ids.shape[1]:].tolist()
+ seq_dict[idx] = texts[i]
+ step_dict[idx] = step_map
+
+ # free unused GPU cache
+ torch.cuda.empty_cache()
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+ mp.set_start_method("spawn", force=True)
+
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ project_name = config.experiment.project
+
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>You need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+
+
+
+ code_task = False
+ if config.experiment.function == "train":
+ dataset = config.dataset.train_dataset
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ if config.dataset.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+ elif config.experiment.function == "evaluation":
+ dataset = config.evaluation.eval_dataset
+ if config.evaluation.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+ k_sample = config.evaluation.num_response_per_task
+ batch_size = config.evaluation.batch_size
+
+ config.rollout.steps = config.evaluation.steps
+ config.rollout.temperature = config.evaluation.temperature
+ config.rollout.target = config.evaluation.target
+ config.rollout.block_size = config.evaluation.block_size
+ config.rollout.use_cache = config.evaluation.use_cache
+ config.rollout.further_horizon = config.evaluation.further_horizon
+ config.rollout.remasking_strategy = config.evaluation.remasking_strategy
+ config.rollout.dynamic_threshold = config.evaluation.dynamic_threshold
+ config.rollout.target = config.evaluation.target
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(8)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ if config.experiment.function == "train":
+ random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+
+ if config.experiment.function == "train":
+ random_select_num = config.rollout.num_task_per_step
+ random_select_num = int(random_select_num / num_node)
+ random_select_num = min(random_select_num, len(data))
+ data = random_select(data, random_select_num)
+ num = len(data)
+
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_task:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+ # --------------------- 2. split to each GPU ----------------------
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1, "need >=2 GPUs for parallel inference"
+
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+
+
+
+ # ------------------- 4. launch all workers -----------------------
+ manager = mp.Manager()
+ seq_dict = manager.dict() # {shuffled_pos: text}
+ step_dict = manager.dict() # {shuffled_pos: step_map}
+ procs = []
+
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ seq_dict,
+ step_dict,
+ batch_size,
+ config))
+ p.start()
+ procs.append(p)
+
+ for p in procs:
+ p.join()
+
+ # ------------------- 5. restore original order -------------------
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import re
+
+ # calculate the response length (ignoring repeated <|endoftext|> tokens)
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_task:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ elif data[int(i/k_sample)]["test_method"] == "stdio":
+ extracted_output = extract_code(full_output)
+ else:
+ extracted_output = extract_final_boxed_answer(full_output)
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
diff --git a/sample/lladav_rl_rollout.py b/sample/lladav_rl_rollout.py
new file mode 100644
index 0000000000000000000000000000000000000000..d53225f1f553afe29cc08b2c42bf1944d548a3dc
--- /dev/null
+++ b/sample/lladav_rl_rollout.py
@@ -0,0 +1,659 @@
+from __future__ import annotations
+import math, json, os, time
+from dataclasses import dataclass, asdict
+from typing import List, Tuple
+import numpy as np
+from jinja2 import Template
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+from transformers import AutoTokenizer
+from llava.model.builder import load_pretrained_model
+from llava.mm_utils import process_images, tokenizer_image_token
+from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
+from llava.conversation import conv_templates
+from llava.cache import dLLMCache, dLLMCacheConfig
+from llava.hooks.fast_dllm_hook import register_fast_dllm_hook, unregister_fast_dllm_hook
+from llava.hooks import register_cache_LLaDA_V
+from torchvision import transforms
+import multiprocessing as mp
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+
+from transformers.utils import logging as hf_logging
+hf_logging.set_verbosity_error()
+
+import warnings
+warnings.filterwarnings("ignore", category=UserWarning)
+warnings.filterwarnings("ignore", category=FutureWarning)
+
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def add_gumbel_noise(logits, temperature):
+ if temperature == 0:
+ return logits
+ logits = logits.to(torch.float64)
+ noise = torch.rand_like(logits, dtype=torch.float64)
+ noise = (- torch.log(noise)) ** temperature
+ return logits.exp() / noise
+
+
+
+def get_num_transfer_tokens(mask_index, steps):
+ mask_num = mask_index.sum(dim=1, keepdim=True)
+
+ base = mask_num // steps
+ remainder = mask_num % steps
+
+ num_transfer_tokens = torch.zeros(mask_num.size(0), steps, device=mask_index.device, dtype=torch.int64) + base
+
+ for i in range(mask_num.size(0)):
+ num_transfer_tokens[i, :remainder[i]] += 1
+
+ return num_transfer_tokens
+
+
+
+# ──────────────────────────── return type ────────────────────────────────
+@dataclass
+class DiffusionOutput:
+ sequences: torch.Tensor # final result (B, L_total) (GPU)
+ history: List[torch.Tensor] # all intermediate x (CPU)
+ nfe: int
+ L0: int # length of prompt
+
+def build_llava_prompt(question: str, conv_template: str = "llava_llada"):
+ import copy
+ conv = copy.deepcopy(conv_templates[conv_template])
+ conv.append_message(conv.roles[0], DEFAULT_IMAGE_TOKEN + "\n" + question)
+ conv.append_message(conv.roles[1], None)
+ return conv.get_prompt()
+
+@torch.no_grad()
+def generate_llada_v_with_history(
+ model, tokenizer, input_ids, images, image_sizes,
+ steps, gen_length, block_length, temperature,
+ target, unmask_threshold=None, mask_id=126336
+) -> DiffusionOutput:
+ position_ids = None
+ attention_mask = None
+ inputs_embeds = None
+ (_input_ids, position_ids, attention_mask, _pkv, inputs_embeds, _labels) = model.prepare_inputs_labels_for_multimodal(
+ input_ids, position_ids, attention_mask, None, None, images, ["image"], image_sizes=image_sizes
+ )
+ assert input_ids.shape[1] < inputs_embeds.shape[1], "inputs_embeds must be longer than input_ids, because it contains image embeddings."
+ device = inputs_embeds.device
+ B = inputs_embeds.shape[0]
+ L0 = inputs_embeds.shape[1]
+ d = inputs_embeds.shape[2]
+
+ assert B == 1, "LLaDA-V only support batch size 1 for now"
+
+ total_len = L0 + gen_length
+ masked_embed = model.get_model().embed_tokens(torch.tensor([mask_id], device=device)) # (1, d)
+ x_embeds = masked_embed.repeat(B, total_len, 1)
+ x_embeds[:, :L0] = inputs_embeds
+ x_tokens = torch.full((B, total_len), mask_id, dtype=torch.long, device=device)
+ prompt_index = torch.zeros_like(x_tokens, dtype=torch.bool)
+ prompt_index[:, :L0] = True
+
+ assert gen_length % block_length == 0
+ num_blocks = gen_length // block_length
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (i < rem) for i in range(num_blocks)]
+
+ nfe = 0
+ hist: List[torch.Tensor] = []
+
+ for blk in range(num_blocks):
+ s = L0 + blk * block_length
+ e = L0 + (blk + 1) * block_length
+
+ cur_steps = steps_per_block[blk]
+
+ outputs = model.get_model()(inputs_embeds=x_embeds, attention_mask=None, position_ids=None, use_cache=False, return_dict=True)
+ logits = model.lm_head(outputs.last_hidden_state).float()
+
+ blk_tokens = x_tokens[:, s:e]
+ eos_mask = (blk_tokens == 126348)
+ if eos_mask.any():
+ first_eos_pos = torch.where(eos_mask[0])[0][0].item()
+ valid_region = torch.zeros_like(blk_tokens, dtype=torch.bool)
+ valid_region[:, :first_eos_pos] = True
+ else:
+ valid_region = torch.ones_like(blk_tokens, dtype=torch.bool)
+
+ mask_blk = (blk_tokens == mask_id) & valid_region
+ num_transfer = get_num_transfer_tokens(mask_blk, cur_steps)
+
+ x0_blk, tr_idx_blk = get_transfer_index(
+ logits[:, s:e], temperature, target,
+ mask_blk, blk_tokens, num_transfer[:, 0],
+ threshold=unmask_threshold
+ )
+ x_slice = x_tokens[:, s:e]
+ x_slice[tr_idx_blk] = x0_blk[tr_idx_blk]
+ x_tokens[:, s:e] = x_slice
+ x_embeds[:, s:e] = model.get_model().embed_tokens(x_tokens[:, s:e])
+ hist.append(x_tokens.clone().cpu())
+ nfe += 1
+
+ i = 1
+ while i < cur_steps:
+ nfe += 1
+ outputs = model.get_model()(inputs_embeds=x_embeds, attention_mask=None, position_ids=None, use_cache=False, return_dict=True)
+ logits = model.lm_head(outputs.last_hidden_state).float()
+
+ blk_tokens = x_tokens[:, s:e]
+ eos_mask = (blk_tokens == 126348)
+ if eos_mask.any():
+ first_eos_pos = torch.where(eos_mask[0])[0][0].item()
+ valid_region = torch.zeros_like(blk_tokens, dtype=torch.bool)
+ valid_region[:, :first_eos_pos] = True
+ else:
+ valid_region = torch.ones_like(blk_tokens, dtype=torch.bool)
+
+ mask_blk = (x_tokens[:, s:] == mask_id)
+ mask_blk[:, block_length:] = False
+ mask_blk[:, :block_length] = mask_blk[:, :block_length] & valid_region
+
+ x0, tr_idx = get_transfer_index(
+ logits[:, s:], temperature, target,
+ mask_blk, x_tokens[:, s:], num_transfer[:, i],
+ threshold=unmask_threshold
+ )
+
+ if tr_idx.any():
+ x_slice = x_tokens[:, s:]
+ x_slice[tr_idx] = x0[tr_idx]
+ x_tokens[:, s:] = x_slice
+ x0_embeds = model.get_model().embed_tokens(torch.where(tr_idx, x0, x_slice))
+ x_embeds[:, s:][tr_idx] = x0_embeds[tr_idx]
+
+ hist.append(x_tokens.clone().cpu())
+
+ remaining_masks = (x_tokens[:, s:e] == mask_id) & valid_region
+ if remaining_masks.sum() == 0:
+ break
+ i += 1
+
+ if (x_tokens[:, s:e] == 126348).any():
+ break
+
+
+ return DiffusionOutput(sequences=x_tokens, history=hist, nfe=nfe, L0=L0)
+
+
+
+
+def get_transfer_index(logits, temperature, target, mask_index, x, num_transfer_tokens, threshold=None):
+ logits_with_noise = add_gumbel_noise(logits, temperature=temperature)
+ logits_with_noise[..., 126336] = -float('inf') # 禁止生成 mask_id
+ x0 = torch.argmax(logits_with_noise, dim=-1) # b, l
+
+ if target == 'confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = torch.squeeze(
+ torch.gather(p, dim=-1, index=torch.unsqueeze(x0, -1)), -1) # b, l
+ elif target == 'margin_confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ top2 = torch.topk(p, 2, dim=-1).values # (b, l, 2)
+ x0_p = top2[..., 0] - top2[..., 1] # Δ(top1, top2)
+ elif target == 'neg_entropy':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = -torch.sum(p * torch.log(p + 1e-10), dim=-1) # –entropy
+ elif target == 'random':
+ x0_p = torch.rand((x0.shape[0], x0.shape[1]), device=x0.device)
+ else:
+ raise NotImplementedError(target)
+
+ x0 = torch.where(mask_index, x0, x)
+
+ if threshold is not None:
+ selected = mask_index & (x0_p >= threshold) # (B, T)
+
+ for j in range(selected.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+
+ num_selected = selected[j].sum().item()
+ if num_selected < k:
+ masked_scores = x0_p[j].masked_fill(~mask_index[j], float("-inf"))
+ _, top_indices = torch.topk(masked_scores, k=k)
+ selected[j, :] = False
+ selected[j, top_indices] = True
+
+ return x0, selected
+
+
+ confidence = x0_p.masked_fill(~mask_index, float("-inf"))
+ transfer_index = torch.zeros_like(x0, dtype=torch.bool, device=x0.device)
+ for j in range(confidence.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+ _, sel = torch.topk(confidence[j], k=k)
+ transfer_index[j, sel] = True
+ return x0, transfer_index
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem=data_i["question"])
+
+
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+ for t, snap in enumerate(history, start=1):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ unprocessed_mask = (step_map == 0)
+ if unprocessed_mask.any():
+ max_step = step_map.max()
+ step_map[unprocessed_mask] = max_step
+
+ return step_map
+
+
+
+from tqdm import tqdm
+
+def worker(pretrained_model, rank, prompts, orig_idx, data_idx, image_paths, seq_dict, step_dict, imgabs_dict, batch_size, config):
+ from PIL import Image
+ import copy
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+
+ tokenizer_gpu, model_gpu, image_processor, max_length = load_pretrained_model(
+ pretrained_model, None, "llava_llada", attn_implementation="sdpa", device_map=device
+ )
+ model_gpu.eval()
+ special_tokens = {
+ "additional_special_tokens": [DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN]
+ }
+ num_new = tokenizer_gpu.add_special_tokens(special_tokens)
+ if num_new > 0:
+ model_gpu.resize_token_embeddings(len(tokenizer_gpu))
+
+
+ use_fast_dllm = getattr(config.rollout, "use_fast_dllm", False)
+ use_dllm_cache = getattr(config.rollout, "use_dllm_cache", False)
+ if use_fast_dllm:
+ register_fast_dllm_hook(model_gpu)
+ elif use_dllm_cache:
+ dLLMCache.new_instance(**asdict(dLLMCacheConfig(
+ prompt_interval_steps=getattr(config.rollout, "prompt_interval_steps", 25),
+ gen_interval_steps=getattr(config.rollout, "gen_interval_steps", 7),
+ transfer_ratio=getattr(config.rollout, "transfer_ratio", 0.25),
+ )))
+ register_cache_LLaDA_V(model_gpu, "model.layers")
+
+ mask_id = 126336
+ unmask_threshold = None if config.rollout.remasking_strategy == "low_confidence_static" else config.rollout.dynamic_threshold
+
+ for j in tqdm(range(len(prompts)), desc=f"GPU {rank}", position=rank, leave=True):
+ did = data_idx[j]
+ prompt_text = prompts[j]
+ img_path = image_paths[j] if image_paths is not None else None
+
+ if img_path is not None:
+ if not os.path.isabs(img_path) and hasattr(config.dataset, "image_root") and config.dataset.image_root is not None:
+ img_path = os.path.join(config.dataset.image_root, img_path)
+ img_path = os.path.abspath(img_path)
+ imgabs_dict[did] = img_path
+
+ image = Image.open(img_path).convert("RGB")
+ image_tensor = process_images([image], image_processor, model_gpu.config)
+ image_tensor = [_image.to(dtype=torch.float16, device=device) for _image in image_tensor]
+ image_sizes = [image.size]
+ else:
+ image_tensor = None
+ image_sizes = None
+
+ conv_prompt = build_llava_prompt(prompt_text, conv_template="llava_llada")
+ input_ids = tokenizer_image_token(conv_prompt, tokenizer_gpu, IMAGE_TOKEN_INDEX, return_tensors="pt").unsqueeze(0).to(device)
+
+ # 生成(带历史)
+ out = generate_llada_v_with_history(
+ model=model_gpu,
+ tokenizer=tokenizer_gpu,
+ input_ids=input_ids,
+ images=image_tensor,
+ image_sizes=image_sizes,
+ steps=config.rollout.steps,
+ gen_length=config.rollout.max_gen_length,
+ block_length=config.rollout.block_size,
+ temperature=config.rollout.temperature,
+ target=config.rollout.target,
+ unmask_threshold=unmask_threshold,
+ mask_id=mask_id,
+ )
+
+ out.sequences = out.sequences.cpu()
+ L0 = model_gpu.prepare_inputs_labels_for_multimodal(input_ids, None, None, None, None, image_tensor, ["image"],
+ image_sizes=image_sizes)[4].shape[1]
+ assert L0 == out.L0, "L0 must be equal to the length of prompt."
+ seq_ids = out.sequences[:, L0:].tolist()
+ text = tokenizer_gpu.batch_decode(seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)[0]
+
+ # step_map
+ m = denoise_step_map(out.history, mask_id=mask_id, sample_idx=0)
+ step_map = m[L0:].tolist()
+
+ seq_dict[orig_idx[j]] = text
+ step_dict[orig_idx[j]] = step_map
+
+ torch.cuda.empty_cache()
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+ mp.set_start_method("spawn", force=True)
+
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ project_name = config.experiment.project
+
+ if config.reward.answer_must_in_box:
+ system_prompts = """You need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}"""
+ else:
+ system_prompts = """{{problem}}"""
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+
+ assert config.dataset.data_type != "code", "lladav is not supported for code tasks."
+
+ code_task = False
+ if config.experiment.function == "train":
+ dataset = config.dataset.train_dataset
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ if config.dataset.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+
+ elif config.experiment.function == "evaluation":
+ dataset = config.evaluation.eval_dataset
+ if config.evaluation.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+ k_sample = config.evaluation.num_response_per_task
+ batch_size = config.evaluation.batch_size
+
+ config.rollout.steps = config.evaluation.steps
+ config.rollout.temperature = config.evaluation.temperature
+ config.rollout.target = config.evaluation.target
+ config.rollout.block_size = config.evaluation.block_size
+ config.rollout.use_cache = config.evaluation.use_cache
+ config.rollout.further_horizon = config.evaluation.further_horizon
+ config.rollout.remasking_strategy = config.evaluation.remasking_strategy
+ config.rollout.dynamic_threshold = config.evaluation.dynamic_threshold
+ config.rollout.target = config.evaluation.target
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+
+
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(8)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ if config.experiment.function == "train":
+ random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+
+ if config.experiment.function == "train":
+ random_select_num = config.rollout.num_task_per_step
+ random_select_num = int(random_select_num / num_node)
+ random_select_num = min(random_select_num, len(data))
+ data = random_select(data, random_select_num)
+ num = len(data)
+
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_task:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+ if config.model.model_base in ["llava_llada", "llada_v", "llada-v", "lladav"] and (config.dataset.data_type == "mmu" or config.evaluation.data_type == "mmu"):
+ image_paths = []
+ for i in range(num):
+ image_paths += [data[i]["image"]] * k_sample
+ else:
+ image_paths = None
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+ # --------------------- 2. split to each GPU ----------------------
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1, "need >=2 GPUs for parallel inference"
+
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+ data_idx_full = [index_list[i] for i in shuffled_idx]
+ data_idx_chunks = split_even(data_idx_full, n_gpu)
+ if image_paths is not None:
+ image_paths_full = [image_paths[i] for i in shuffled_idx]
+ image_chunks = split_even(image_paths_full, n_gpu)
+ else:
+ image_chunks = [None] * n_gpu
+
+
+ # ------------------- 4. launch all workers -----------------------
+ manager = mp.Manager()
+ seq_dict = manager.dict() # {shuffled_pos: text}
+ step_dict = manager.dict() # {shuffled_pos: step_map}
+ imgabs_dict = manager.dict() # {data_idx: absolute_image_path (str)}
+ procs = []
+
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ data_idx_chunks[rk],
+ image_chunks[rk],
+ seq_dict,
+ step_dict,
+ imgabs_dict,
+ batch_size,
+ config))
+
+ p.start()
+ procs.append(p)
+
+ for p in procs:
+ p.join()
+
+ # ------------------- 5. restore original order -------------------
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+ if (config.model.model_base in ["llava_llada", "llada_v", "llada-v", "lladav"] and (config.dataset.data_type == "mmu" or config.evaluation.data_type == "mmu")):
+ for di in range(num):
+ if di in imgabs_dict:
+ data[di]["image_abs_path"] = imgabs_dict[di]
+
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import re
+
+ # calculate the response length (ignoring repeated <|endoftext|> tokens)
+ def get_token_lengths(strings, tokenizer):
+ pad_token = "<|mdm_mask|>"
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_task:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ elif data[int(i/k_sample)]["test_method"] == "stdio":
+ extracted_output = extract_code(full_output)
+ else:
+ if config.reward.answer_must_in_box:
+ extracted_output = extract_final_boxed_answer(full_output)
+ else:
+ if "" in full_output:
+ extracted_output = full_output.split("")[1]
+ else:
+ extracted_output = full_output
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
diff --git a/sample/lladav_sample.py b/sample/lladav_sample.py
new file mode 100644
index 0000000000000000000000000000000000000000..6707382ce5c1b0dee903e0926f2f2457e49e65da
--- /dev/null
+++ b/sample/lladav_sample.py
@@ -0,0 +1,423 @@
+from __future__ import annotations
+import json, os
+from dataclasses import dataclass
+from typing import List
+import random
+from jinja2 import Template
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+from transformers import AutoTokenizer
+from llava.model.builder import load_pretrained_model
+from llava.mm_utils import process_images, tokenizer_image_token
+from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
+from llava.conversation import conv_templates
+import multiprocessing as mp
+from tqdm import tqdm
+import re
+
+from omegaconf import OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def add_gumbel_noise(logits, temperature):
+ if temperature == 0:
+ return logits
+ logits = logits.to(torch.float64)
+ noise = torch.rand_like(logits, dtype=torch.float64)
+ noise = (- torch.log(noise)) ** temperature
+ return logits.exp() / noise
+
+def get_num_transfer_tokens(mask_index, steps):
+ mask_num = mask_index.sum(dim=1, keepdim=True)
+ base = mask_num // steps
+ remainder = mask_num % steps
+ num_transfer_tokens = torch.zeros(mask_num.size(0), steps, device=mask_index.device, dtype=torch.int64) + base
+ for i in range(mask_num.size(0)):
+ num_transfer_tokens[i, :remainder[i]] += 1
+ return num_transfer_tokens
+
+@dataclass
+class DiffusionOutput:
+ sequences: torch.Tensor
+ history: List[torch.Tensor]
+ nfe: int
+ L0: int
+
+def build_llava_prompt(question: str, conv_template: str = "llava_llada"):
+ import copy
+ conv = copy.deepcopy(conv_templates[conv_template])
+ conv.append_message(conv.roles[0], DEFAULT_IMAGE_TOKEN + "\n" + question)
+ conv.append_message(conv.roles[1], None)
+ return conv.get_prompt()
+
+@torch.no_grad()
+def generate_llada_v_with_history(
+ model, tokenizer, input_ids, images, image_sizes,
+ steps, gen_length, block_length, temperature,
+ target, unmask_threshold=None, mask_id=126336
+) -> DiffusionOutput:
+ position_ids = None
+ attention_mask = None
+ inputs_embeds = None
+ (_input_ids, position_ids, attention_mask, _pkv, inputs_embeds, _labels) = model.prepare_inputs_labels_for_multimodal(
+ input_ids, position_ids, attention_mask, None, None, images, ["image"], image_sizes=image_sizes
+ )
+ device = inputs_embeds.device
+ B = inputs_embeds.shape[0]
+ L0 = inputs_embeds.shape[1]
+ d = inputs_embeds.shape[2]
+ assert B == 1
+ total_len = L0 + gen_length
+ masked_embed = model.get_model().embed_tokens(torch.tensor([mask_id], device=device))
+ x_embeds = masked_embed.repeat(B, total_len, 1)
+ x_embeds[:, :L0] = inputs_embeds
+ x_tokens = torch.full((B, total_len), mask_id, dtype=torch.long, device=device)
+ assert gen_length % block_length == 0
+ num_blocks = gen_length // block_length
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (i < rem) for i in range(num_blocks)]
+ nfe = 0
+ hist: List[torch.Tensor] = []
+ for blk in range(num_blocks):
+ s = L0 + blk * block_length
+ e = L0 + (blk + 1) * block_length
+ cur_steps = steps_per_block[blk]
+ outputs = model.get_model()(inputs_embeds=x_embeds, attention_mask=None, position_ids=None, use_cache=False, return_dict=True)
+ logits = model.lm_head(outputs.last_hidden_state).float()
+ blk_tokens = x_tokens[:, s:e]
+ eos_mask = (blk_tokens == 126348)
+ if eos_mask.any():
+ first_eos_pos = torch.where(eos_mask[0])[0][0].item()
+ valid_region = torch.zeros_like(blk_tokens, dtype=torch.bool)
+ valid_region[:, :first_eos_pos] = True
+ else:
+ valid_region = torch.ones_like(blk_tokens, dtype=torch.bool)
+ mask_blk = (blk_tokens == mask_id) & valid_region
+ num_transfer = get_num_transfer_tokens(mask_blk, cur_steps)
+ x0_blk, tr_idx_blk = get_transfer_index(
+ logits[:, s:e], temperature, target,
+ mask_blk, blk_tokens, num_transfer[:, 0],
+ threshold=unmask_threshold
+ )
+ x_slice = x_tokens[:, s:e]
+ x_slice[tr_idx_blk] = x0_blk[tr_idx_blk]
+ x_tokens[:, s:e] = x_slice
+ x_embeds[:, s:e] = model.get_model().embed_tokens(x_tokens[:, s:e])
+ hist.append(x_tokens.clone().cpu())
+ nfe += 1
+ i = 1
+ while i < cur_steps:
+ nfe += 1
+ outputs = model.get_model()(inputs_embeds=x_embeds, attention_mask=None, position_ids=None, use_cache=False, return_dict=True)
+ logits = model.lm_head(outputs.last_hidden_state).float()
+ blk_tokens = x_tokens[:, s:e]
+ eos_mask = (blk_tokens == 126348)
+ if eos_mask.any():
+ first_eos_pos = torch.where(eos_mask[0])[0][0].item()
+ valid_region = torch.zeros_like(blk_tokens, dtype=torch.bool)
+ valid_region[:, :first_eos_pos] = True
+ else:
+ valid_region = torch.ones_like(blk_tokens, dtype=torch.bool)
+ mask_blk = (x_tokens[:, s:] == mask_id)
+ mask_blk[:, block_length:] = False
+ mask_blk[:, :block_length] = mask_blk[:, :block_length] & valid_region
+ x0, tr_idx = get_transfer_index(
+ logits[:, s:], temperature, target,
+ mask_blk, x_tokens[:, s:], num_transfer[:, i],
+ threshold=unmask_threshold
+ )
+ if tr_idx.any():
+ x_slice = x_tokens[:, s:]
+ x_slice[tr_idx] = x0[tr_idx]
+ x_tokens[:, s:] = x_slice
+ x0_embeds = model.get_model().embed_tokens(torch.where(tr_idx, x0, x_slice))
+ x_embeds[:, s:][tr_idx] = x0_embeds[tr_idx]
+ hist.append(x_tokens.clone().cpu())
+ remaining_masks = (x_tokens[:, s:e] == mask_id) & valid_region
+ if remaining_masks.sum() == 0:
+ break
+ i += 1
+ if (x_tokens[:, s:e] == 126348).any():
+ break
+ return DiffusionOutput(sequences=x_tokens, history=hist, nfe=nfe, L0=L0)
+
+def get_transfer_index(logits, temperature, target, mask_index, x, num_transfer_tokens, threshold=None):
+ logits_with_noise = add_gumbel_noise(logits, temperature=temperature)
+ logits_with_noise[..., 126336] = -float('inf')
+ x0 = torch.argmax(logits_with_noise, dim=-1)
+ if target == 'confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = torch.squeeze(
+ torch.gather(p, dim=-1, index=torch.unsqueeze(x0, -1)), -1)
+ elif target == 'margin_confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ top2 = torch.topk(p, 2, dim=-1).values
+ x0_p = top2[..., 0] - top2[..., 1]
+ elif target == 'neg_entropy':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = -torch.sum(p * torch.log(p + 1e-10), dim=-1)
+ elif target == 'random':
+ x0_p = torch.rand((x0.shape[0], x0.shape[1]), device=x0.device)
+ else:
+ raise NotImplementedError(target)
+ x0 = torch.where(mask_index, x0, x)
+ if threshold is not None:
+ selected = mask_index & (x0_p >= threshold)
+ for j in range(selected.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+ num_selected = selected[j].sum().item()
+ if num_selected < k:
+ masked_scores = x0_p[j].masked_fill(~mask_index[j], float("-inf"))
+ _, top_indices = torch.topk(masked_scores, k=k)
+ selected[j, :] = False
+ selected[j, top_indices] = True
+ return x0, selected
+ confidence = x0_p.masked_fill(~mask_index, float("-inf"))
+ transfer_index = torch.zeros_like(x0, dtype=torch.bool, device=x0.device)
+ for j in range(confidence.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+ _, sel = torch.topk(confidence[j], k=k)
+ transfer_index[j, sel] = True
+ return x0, transfer_index
+
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem=data_i["question"])
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag)
+ if start == -1:
+ return "Can not extract the answer!"
+ i = start + len(tag)
+ depth = 1
+ buf = []
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0:
+ break
+ buf.append(ch)
+ i += 1
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+ for t, snap in enumerate(history, start=1):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ unprocessed_mask = (step_map == 0)
+ if unprocessed_mask.any():
+ max_step = step_map.max()
+ step_map[unprocessed_mask] = max_step
+ return step_map
+
+def worker(pretrained_model, rank, prompts, orig_idx, data_idx, image_paths, seq_dict, step_dict, imgabs_dict, batch_size, config):
+ from PIL import Image
+ import copy
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+ tokenizer_gpu, model_gpu, image_processor, max_length = load_pretrained_model(
+ pretrained_model, None, "llava_llada", attn_implementation="sdpa", device_map=device
+ )
+ model_gpu.eval()
+ special_tokens = {
+ "additional_special_tokens": [DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN]
+ }
+ num_new = tokenizer_gpu.add_special_tokens(special_tokens)
+ if num_new > 0:
+ try:
+ model_gpu.resize_token_embeddings(len(tokenizer_gpu), mean_resizing=False)
+ except TypeError:
+ model_gpu.resize_token_embeddings(len(tokenizer_gpu))
+ mask_id = 126336
+ unmask_threshold = None if config.rollout.remasking_strategy == "low_confidence_static" else config.rollout.dynamic_threshold
+ for j in tqdm(range(len(prompts)), desc=f"GPU {rank}", position=rank, leave=True):
+ did = data_idx[j]
+ prompt_text = prompts[j]
+ img_path = image_paths[j] if image_paths is not None else None
+ if img_path is not None:
+ if not os.path.isabs(img_path) and hasattr(config.dataset, "image_root") and config.dataset.image_root is not None:
+ img_path = os.path.join(config.dataset.image_root, img_path)
+ img_path = os.path.abspath(img_path)
+ imgabs_dict[did] = img_path
+ image = Image.open(img_path).convert("RGB")
+ image_tensor = process_images([image], image_processor, model_gpu.config)
+ image_tensor = [_image.to(dtype=torch.float16, device=device) for _image in image_tensor]
+ image_sizes = [image.size]
+ else:
+ image_tensor = None
+ image_sizes = None
+ conv_prompt = build_llava_prompt(prompt_text, conv_template="llava_llada")
+ input_ids = tokenizer_image_token(conv_prompt, tokenizer_gpu, IMAGE_TOKEN_INDEX, return_tensors="pt").unsqueeze(0).to(device)
+ out = generate_llada_v_with_history(
+ model=model_gpu,
+ tokenizer=tokenizer_gpu,
+ input_ids=input_ids,
+ images=image_tensor,
+ image_sizes=image_sizes,
+ steps=config.rollout.steps,
+ gen_length=config.rollout.max_gen_length,
+ block_length=config.rollout.block_size,
+ temperature=config.rollout.temperature,
+ target=config.rollout.target,
+ unmask_threshold=unmask_threshold,
+ mask_id=mask_id,
+ )
+ out.sequences = out.sequences.cpu()
+ L0 = model_gpu.prepare_inputs_labels_for_multimodal(input_ids, None, None, None, None, image_tensor, ["image"], image_sizes=image_sizes)[4].shape[1]
+ seq_ids = out.sequences[:, L0:].tolist()
+ text = tokenizer_gpu.batch_decode(seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)[0]
+ m = denoise_step_map(out.history, mask_id=mask_id, sample_idx=0)
+ step_map = m[L0:].tolist()
+ seq_dict[orig_idx[j]] = text
+ step_dict[orig_idx[j]] = step_map
+ torch.cuda.empty_cache()
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+if __name__ == "__main__":
+ config = get_config()
+ mp.set_start_method("spawn", force=True)
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+ project_name = config.experiment.project
+ if config.answer_must_in_box:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>You need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+ else:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+ dataset = config.dataset.eval_dataset
+ pretrained_model = config.model
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ data = get_data_chunk(data, num_node, node_index)
+ num = len(data)
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+ image_paths = []
+ for i in range(num):
+ image_paths += [data[i].get("image") or data[i].get("image_path")] * k_sample
+ cprint("start generation...", "green")
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+ data_idx_full = [index_list[i] for i in shuffled_idx]
+ data_idx_chunks = split_even(data_idx_full, n_gpu)
+ image_paths_full = [image_paths[i] for i in shuffled_idx]
+ image_chunks = split_even(image_paths_full, n_gpu)
+ manager = mp.Manager()
+ seq_dict = manager.dict()
+ step_dict = manager.dict()
+ imgabs_dict = manager.dict()
+ procs = []
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ data_idx_chunks[rk],
+ image_chunks[rk],
+ seq_dict,
+ step_dict,
+ imgabs_dict,
+ batch_size,
+ config))
+ p.start()
+ procs.append(p)
+ for p in procs:
+ p.join()
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+ for di in range(num):
+ if di in imgabs_dict:
+ data[di]["image_abs_path"] = imgabs_dict[di]
+ def get_token_lengths(strings, tokenizer):
+ pad_token = "<|mdm_mask|>"
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+ collapse_re = re.compile(pattern)
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ i = 0
+ for full_output in restored_outputs:
+ if config.answer_must_in_box:
+ extracted_output = extract_final_boxed_answer(full_output)
+ else:
+ if "" in full_output:
+ extracted_output = full_output.split("")[1]
+ else:
+ extracted_output = full_output
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+ outputs_name = outputs_name + "-" + config.rollout.remasking_strategy
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
diff --git a/sample/llava_next_video_rl_rollout_video_reversal.py b/sample/llava_next_video_rl_rollout_video_reversal.py
new file mode 100644
index 0000000000000000000000000000000000000000..109516343cbd5ec20bd959f06f8908012bde1e35
--- /dev/null
+++ b/sample/llava_next_video_rl_rollout_video_reversal.py
@@ -0,0 +1,212 @@
+import copy
+import os
+import re
+import sys
+import warnings
+from math import ceil
+import random
+
+import torch
+
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+LLAVA_NEXT_ROOT = os.path.join(REPO_ROOT, "LLaVA-NeXT")
+sys.path.insert(0, LLAVA_NEXT_ROOT)
+sys.path.insert(1, REPO_ROOT)
+
+from llava.constants import DEFAULT_IMAGE_TOKEN, IMAGE_TOKEN_INDEX
+from llava.conversation import conv_templates
+from llava.mm_utils import tokenizer_image_token
+from llava.model.builder import load_pretrained_model
+from llava.utils import disable_torch_init, process_video_with_decord
+
+from train.llava_next_video.common import (
+ build_video_mcq_prompt,
+ dump_json,
+ ensure_dir,
+ get_config,
+ get_rollout_output_path,
+ get_stage_dataset_path,
+ load_json,
+ resolve_media_abs_path,
+)
+
+warnings.filterwarnings("ignore", message=".*copying from a non-meta parameter.*")
+warnings.filterwarnings("ignore", message=".*Flash Attention 2.0 with a model not initialized on GPU.*")
+
+
+def extract_option_answer(text: str) -> str:
+ clean = str(text).replace("<|eot_id|>", "").strip()
+ box_match = re.search(r"\s*([A-Z])\s*", clean, flags=re.I)
+ if box_match:
+ return box_match.group(1).upper()
+ letter_match = re.search(r"\b([A-D])\b", clean.upper())
+ if letter_match:
+ return letter_match.group(1)
+ return clean
+
+
+def load_video_tensor(video_path: str, image_processor, model_cfg, device):
+ if not hasattr(model_cfg, "video_fps"):
+ setattr(model_cfg, "video_fps", 1)
+ if not hasattr(model_cfg, "frames_upbound"):
+ setattr(model_cfg, "frames_upbound", 16)
+ if not hasattr(model_cfg, "force_sample"):
+ setattr(model_cfg, "force_sample", True)
+ video, video_time, frame_time, num_frames_to_sample = process_video_with_decord(video_path, model_cfg)
+ pixel_values = image_processor.preprocess(video, return_tensors="pt")["pixel_values"].to(device=device, dtype=torch.float16)
+ return [pixel_values], [video[0].size], video_time, frame_time, num_frames_to_sample
+
+
+def load_video_tensor_with_optional_shuffle(video_path: str, image_processor, model_cfg, device, is_shuffled: bool):
+ if not hasattr(model_cfg, "video_fps"):
+ setattr(model_cfg, "video_fps", 1)
+ if not hasattr(model_cfg, "frames_upbound"):
+ setattr(model_cfg, "frames_upbound", 16)
+ if not hasattr(model_cfg, "force_sample"):
+ setattr(model_cfg, "force_sample", True)
+ video, video_time, frame_time, num_frames_to_sample = process_video_with_decord(video_path, model_cfg)
+ if is_shuffled and len(video) > 1:
+ order = list(range(len(video)))
+ random.shuffle(order)
+ video = [video[i] for i in order]
+ frame_time = [frame_time[i] for i in order] if isinstance(frame_time, list) else frame_time
+ pixel_values = image_processor.preprocess(video, return_tensors="pt")["pixel_values"].to(device=device, dtype=torch.float16)
+ return [pixel_values], [video[0].size], video_time, frame_time, num_frames_to_sample
+
+
+def build_input_ids(tokenizer, prompt_text: str, conv_mode: str) -> torch.Tensor:
+ conv = copy.deepcopy(conv_templates[conv_mode])
+ conv.append_message(conv.roles[0], f"{DEFAULT_IMAGE_TOKEN}\n{prompt_text}")
+ conv.append_message(conv.roles[1], None)
+ prompt = conv.get_prompt()
+ return tokenizer_image_token(prompt, tokenizer, IMAGE_TOKEN_INDEX, return_tensors="pt").unsqueeze(0).cuda()
+
+
+def extract_generated_ids(output_ids: torch.Tensor, prompt_ids: torch.Tensor) -> torch.Tensor:
+ # LlavaLlamaForCausalLM.generate() passes `inputs_embeds` into HF generation.
+ # In that path, returned sequences may contain only newly generated tokens,
+ # not `prompt_ids + generated_ids`, so slicing by prompt length can erase
+ # the whole answer.
+ if output_ids.shape[1] > prompt_ids.shape[1]:
+ return output_ids[0, prompt_ids.shape[1]:]
+ return output_ids[0]
+
+
+if __name__ == "__main__":
+ config = get_config()
+ disable_torch_init()
+
+ run_type = config.experiment.function
+ current_epoch = int(config.experiment.current_epoch)
+ dataset = load_json(get_stage_dataset_path(config, run_type))
+ output_path = get_rollout_output_path(config, run_type, current_epoch)
+ ensure_dir(os.path.dirname(output_path))
+ worker_id = int(config.rollout.get("worker_id", 0))
+ num_workers = int(config.rollout.get("num_workers", 1))
+
+ model_path = config.model.pretrained_model if current_epoch == 1 else os.path.join(config.experiment.project, "ckpt", config.model.optimized_name)
+ overwrite_config = dict(config.model.get("load_overwrite_config", {}))
+ tokenizer, model, image_processor, _ = load_pretrained_model(
+ model_path,
+ config.model.get("model_base", None),
+ config.model.model_name,
+ device_map=None,
+ overwrite_config=overwrite_config if overwrite_config else None,
+ )
+ model.eval().cuda()
+
+ results = []
+ max_samples = config.rollout.num_task_per_step if run_type == "train" else config.evaluation.num_task_per_step
+ num_response_per_task = config.rollout.num_response_per_task if run_type == "train" else config.evaluation.num_response_per_task
+ temperature = config.rollout.temperature if run_type == "train" else config.evaluation.temperature
+ max_new_tokens = config.rollout.max_new_tokens if run_type == "train" else config.evaluation.max_new_tokens
+ selected_dataset = dataset[:max_samples]
+ expanded_dataset = []
+ for sample in selected_dataset:
+ ordered = dict(sample)
+ ordered["is_shuffled"] = False
+ expanded_dataset.append(ordered)
+ shuffled = dict(sample)
+ shuffled["is_shuffled"] = True
+ expanded_dataset.append(shuffled)
+ selected_dataset = expanded_dataset
+ shard_size = ceil(len(selected_dataset) / max(num_workers, 1)) if selected_dataset else 0
+ shard_start = worker_id * shard_size
+ shard_end = min(len(selected_dataset), shard_start + shard_size)
+ selected_dataset = selected_dataset[shard_start:shard_end]
+ print(
+ f"[rollout] epoch={current_epoch} split={run_type} worker={worker_id + 1}/{num_workers} "
+ f"samples={len(selected_dataset)} responses_per_task={num_response_per_task}",
+ flush=True,
+ )
+
+ ordered_seen = 0
+ shuffled_seen = 0
+ for sample_idx, sample in enumerate(selected_dataset, start=1):
+ if bool(sample.get("is_shuffled", False)):
+ shuffled_seen += 1
+ else:
+ ordered_seen += 1
+ video_path = resolve_media_abs_path(sample, None, media_type="video")
+ images, image_sizes, video_time, frame_time, num_frames = load_video_tensor_with_optional_shuffle(
+ video_path,
+ image_processor,
+ model.config,
+ model.device,
+ bool(sample.get("is_shuffled", False)),
+ )
+ question = build_video_mcq_prompt(sample)
+ if getattr(model.config, "add_time_instruction", False):
+ time_instruction = (
+ f"The video lasts for {video_time:.2f} seconds, and {num_frames} frames are uniformly sampled from it. "
+ f"These frames are located at {frame_time}. Please answer the following questions related to this video.\n"
+ )
+ question = time_instruction + question
+
+ input_ids = build_input_ids(tokenizer, question, config.model.conv_mode)
+ attention_mask = input_ids.ne(tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0).long()
+
+ full_outputs = []
+ extracted_outputs = []
+ response_lengths = []
+ for _ in range(num_response_per_task):
+ with torch.inference_mode():
+ output_ids = model.generate(
+ inputs=input_ids,
+ images=images,
+ image_sizes=image_sizes,
+ modalities=["video"],
+ attention_mask=attention_mask,
+ do_sample=temperature > 0,
+ temperature=temperature,
+ top_p=config.rollout.top_p,
+ num_beams=1,
+ max_new_tokens=max_new_tokens,
+ use_cache=True,
+ )
+ response_ids = extract_generated_ids(output_ids, input_ids)
+ full_text = tokenizer.decode(response_ids, skip_special_tokens=False).strip()
+ full_outputs.append(full_text)
+ extracted_outputs.append(extract_option_answer(full_text))
+ response_lengths.append(int(response_ids.numel()))
+
+ record = dict(sample)
+ record["video_abs_path"] = video_path
+ record["full_output"] = full_outputs
+ record["extracted_output"] = extracted_outputs
+ record["response_length"] = response_lengths
+ record["is_shuffled"] = bool(sample.get("is_shuffled", False))
+ results.append(record)
+ if sample_idx == 1 or sample_idx % 8 == 0 or sample_idx == len(selected_dataset):
+ print(
+ f"[rollout] epoch={current_epoch} worker={worker_id + 1}/{num_workers} "
+ f"progress={sample_idx}/{len(selected_dataset)} "
+ f"video={os.path.basename(video_path)} shuffled={bool(sample.get('is_shuffled', False))} "
+ f"ordered_seen={ordered_seen} shuffled_seen={shuffled_seen}",
+ flush=True,
+ )
+
+ if num_workers > 1:
+ output_path = output_path.replace(".json", f".part{worker_id}.json")
+ dump_json(output_path, results)
+ print(f"Saved rollout results to {os.path.abspath(output_path)}")
diff --git a/sample/llm_process_reward.py b/sample/llm_process_reward.py
new file mode 100644
index 0000000000000000000000000000000000000000..22139336e0474bd8e78ed009b75c379fe18c6cce
--- /dev/null
+++ b/sample/llm_process_reward.py
@@ -0,0 +1,417 @@
+import os
+import re
+import ast
+import json
+import random
+import argparse
+from jinja2 import Template
+from termcolor import cprint
+import multiprocessing as mp
+from vllm import LLM, SamplingParams
+from transformers import AutoTokenizer
+
+
+
+
+
+
+
+os.environ["TOKENIZERS_PARALLELISM"] = "false"
+
+
+
+
+
+####### vllm inference #######
+
+def worker_fn(pretrained_model, gpu_ids, task_queue, result_queue, max_model_len, max_generation_token, temp):
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, gpu_ids))
+
+ print(f"Loading model on GPUs {gpu_ids}...")
+ llm = LLM(
+ model=pretrained_model,
+ dtype="bfloat16",
+ tensor_parallel_size=len(gpu_ids),
+ gpu_memory_utilization=0.85,
+ max_model_len=max_model_len
+ )
+
+ sampling_params = SamplingParams(
+ temperature=temp,
+ top_p=0.95,
+ top_k=-1,
+ min_p=0.0,
+ max_tokens=max_generation_token,
+ stop=["", "User:", "Human:", "Assistant:", "<|im_end|>", "<|endoftext|>"]
+ )
+
+ while True:
+ task = task_queue.get()
+ if task == "STOP":
+ print("Stopping worker...")
+ break
+ task_id, prompts = task
+ outputs = llm.generate(prompts, sampling_params)
+ result_texts = [out.outputs[0].text for out in outputs]
+ result_queue.put((task_id, result_texts))
+
+# To run the worker setup:
+def start_workers(pretrained_model, gpu_configs, max_model_len, max_generation_token, temp):
+ task_queues = []
+ result_queues = []
+ processes = []
+
+ for i, gpu_ids in enumerate(gpu_configs):
+ task_q = mp.Queue()
+ result_q = mp.Queue()
+ p = mp.Process(
+ target=worker_fn,
+ args=(pretrained_model, gpu_ids, task_q, result_q, max_model_len, max_generation_token, temp)
+ )
+ p.start()
+ task_queues.append(task_q)
+ result_queues.append(result_q)
+ processes.append(p)
+
+ return task_queues, result_queues, processes
+
+# Submit tasks
+def submit_prompt_set(task_queues, prompt_sets):
+ for i, prompts in enumerate(prompt_sets):
+ task_queues[i].put((i, prompts))
+
+# Collect results
+def collect_results(result_queues, num_sets):
+ results = [None] * num_sets
+ for q in result_queues:
+ task_id, result = q.get()
+ results[task_id] = result
+ return results
+
+# Stop workers
+def stop_workers(task_queues, processes):
+ for q in task_queues:
+ q.put("STOP")
+ for p in processes:
+ p.join()
+
+# Split prompts into N chunks
+def split_prompts(prompts, n):
+ k, m = divmod(len(prompts), n)
+ return [prompts[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)]
+
+def get_token_lengths(strings, tokenizer):
+ return [len(tokenizer.encode(s, add_special_tokens=False)) for s in strings]
+
+# vllm inference
+def generate_results(all_prompts, gpu_groups,task_queues, result_queues):
+ prompt_sets = split_prompts(all_prompts, len(gpu_groups))
+ submit_prompt_set(task_queues, prompt_sets)
+ results = collect_results(result_queues, len(prompt_sets))
+ result_list = []
+ for result_set in results:
+ for r in result_set:
+ result_list.append(r)
+ return result_list
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+
+
+
+
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+
+ k_sample = config.rollout.reward_model.k_sample
+ reward_chunk_length = config.rollout.reward_model.reward_chunk_length
+ gpu_groups = [[0,1,2,3],[4,5,6,7]]
+ max_model_len = config.rollout.reward_model.max_model_len
+ max_generation_token = config.rollout.reward_model.max_generation_token
+ temp = config.rollout.reward_model.temperature
+
+ project_name = config.experiment.project
+ dataset = config.dataset.train_dataset
+ reward_llm = config.model.process_reward_model
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+
+ if config.dataset.data_type == "code":
+ raise NotImplementedError("Data type 'code' is not supported for process reward yet.")
+ else:
+ is_code = False
+ system_prompts = """<|im_start|>You are a helpful assistant. <|im_end|>\n<|im_start|>user
+For the question below, you will be given an entire solution (we have already known it is {{if_correct}}) and an excerpt (a middle part) from it. Your task is to grade the excerpt for correctness using 1 (correct) or -1 (incorrect).
+
+Rules:
+- Judge ONLY the excerpt's correctness given the question and the whole solution (which is {{if_correct}}) as context.
+- The excerpt may be truncated at the beginning or end due to chunking; do NOT penalize boundary truncation.
+- If the excerpt contains any error, unjustified inference, or contradiction, score -1. If it is completely correct, score 1.
+- Ignore minor grammar issues that do not affect correctness.
+
+This is the question:
+{{question}}
+
+This is the final ground truth answer:
+{{gt_answer}}
+
+This is the given solution ({{if_correct}}):
+{{whole_solution}}
+
+This is the excerpt I need you to score:
+{{excerpt}}
+
+You need to put your final score in \\boxed{}. <|im_end|>\n<|im_start|>assistant"""
+
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+ # read dataset
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset
+ if num_node > 1:
+ with open("../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json", 'r') as f:
+ data = json.load(f)
+ else:
+ with open("../" + project_name + f"/temp_data/outputs-" + outputs_name + ".json", 'r') as f:
+ data = json.load(f)
+
+ num = len(data)
+
+ # load model, tokenizer, build vllm engines...
+ task_queues, result_queues, processes = start_workers(reward_llm, gpu_groups, max_model_len, max_generation_token, temp)
+
+ # obtain prompt
+ def get_prompt(excerpt, question, correctness, full_output, gt_answer):
+ if correctness:
+ correctness_string = "correct"
+ else:
+ correctness_string = "incorrect"
+ return Template(system_prompts).render(
+ excerpt = excerpt,
+ question = question,
+ if_correct = correctness_string,
+ whole_solution = full_output,
+ gt_answer = gt_answer
+ )
+
+
+
+
+ def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+ # initialization
+ generation_prompts = []
+ index_list = []
+
+
+ def get_prefix_text_by_chunk_end(chunk_end, token_ids) -> str:
+ if chunk_end <= 0:
+ return ""
+
+ return tokenizer.decode(
+ token_ids[:chunk_end],
+ skip_special_tokens=True,
+ clean_up_tokenization_spaces=False
+ )
+
+
+ for i in range(num):
+ # preproces
+ question_i = data[i]["question"]
+ n_j = len(data[i]["full_output"])
+ data[i]["process_reward_list"] = []
+ data[i]["reward_prompt"] = [[] for _ in range(n_j)]
+ for j in range(n_j):
+ data[i]["process_reward_list"].append([])
+ full_output_j = data[i]["full_output"][j]
+ length_j = data[i]["response_length"][j]
+ correctness_j = data[i]["correctness"][j]
+ enc = tokenizer(full_output_j, add_special_tokens=False)
+ token_ids = enc["input_ids"]
+ data[i]["reward_prompt"][j] = []
+ n_k = max(1, int((length_j - 1) / reward_chunk_length + 1))
+ for k in range(n_k):
+ data[i]["process_reward_list"][j].append([])
+ chunk_end = min(length_j, (k + 1) * reward_chunk_length)
+ chunk_text = get_prefix_text_by_chunk_end(chunk_end, token_ids)
+ prompt_text = get_prompt(chunk_text, question_i, correctness_j, full_output_j, data[i]["ground_truth_answer"])
+ for _ in range(k_sample):
+ index_list.append((i, j, k))
+ generation_prompts = generation_prompts + [prompt_text] * k_sample
+ data[i]["reward_prompt"][j].append(prompt_text)
+
+
+
+
+ # sampling process
+
+ cprint("start generation...", "green")
+
+ # shuffle first, to achieve efficiency
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+ indices = list(range(N))
+ shuffled_idx = indices[:]
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+ # generate
+ shuffled_outputs = generate_results(shuffled_prompts, gpu_groups, task_queues, result_queues)
+ restored_outputs = [None] * N
+ for out, idx in zip(shuffled_outputs, shuffled_idx):
+ restored_outputs[idx] = out
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ extracted_score = extract_final_boxed_answer(full_output)
+ i1, j1, k1 = index_list[i]
+ if extracted_score != "1" and extracted_score != "-1":
+ extracted_score = 0.0
+ extracted_score = float(extracted_score)
+ data[i1]["process_reward_list"][j1][k1].append(extracted_score)
+ i += 1
+
+ def all_equal(seq):
+ it = iter(seq)
+ try:
+ first = next(it)
+ except StopIteration:
+ return True
+ return all(x == first for x in it)
+
+ def z_score_normalize(lst):
+ if len(lst) == 1:
+ return lst
+ mean = sum(lst) / len(lst)
+ std = (sum((x - mean) ** 2 for x in lst) / len(lst)) ** 0.5
+ if std == 0:
+ return [0 for x in lst]
+ return [(x - mean) / std for x in lst]
+
+ for i in range(len(data)):
+ max_nk = -1
+ for j in range(len(data[i]["full_output"])):
+ n_k = len(data[i]["process_reward_list"][j])
+ if n_k > max_nk:
+ max_nk = n_k
+ for k in range(n_k):
+ if all_equal(data[i]["process_reward_list"][j][k]):
+ data[i]["process_reward_list"][j][k] = data[i]["process_reward_list"][j][k][0]
+ else:
+ data[i]["process_reward_list"][j][k] = 2.0 * int(data[i]["correctness"][j]) - 1.0
+
+ for k in range(max_nk):
+ target_value = []
+ target_index = []
+ for j in range(len(data[i]["full_output"])):
+ if (k + 1) <= len(data[i]["process_reward_list"][j]):
+ target_value.append(data[i]["process_reward_list"][j][k])
+ target_index.append(j)
+ target_value = z_score_normalize(target_value)
+ for j in target_index:
+ data[i]["process_reward_list"][j][k] = target_value[target_index.index(j)]
+
+
+
+
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+
+
+
+
+ stop_workers(task_queues, processes)
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/mmada_v_rl_rollout.py b/sample/mmada_v_rl_rollout.py
new file mode 100644
index 0000000000000000000000000000000000000000..63c414bc90a5cbac92b1e6badd735eaf25a453ba
--- /dev/null
+++ b/sample/mmada_v_rl_rollout.py
@@ -0,0 +1,654 @@
+from __future__ import annotations
+import math, json, os, time
+from dataclasses import dataclass
+from typing import List, Tuple
+import numpy as np
+from jinja2 import Template
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+from transformers import AutoTokenizer, AutoModel
+from llada.modeling_llada import LLaDAModelLM
+from vq.modeling_magvitv2 import MAGVITv2
+from torchvision import transforms
+import multiprocessing as mp
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def add_gumbel_noise(logits, temperature):
+ if temperature == 0:
+ return logits
+ logits = logits.to(torch.float64)
+ noise = torch.rand_like(logits, dtype=torch.float64)
+ noise = (- torch.log(noise)) ** temperature
+ return logits.exp() / noise
+
+SOI_ID = 126084
+EOI_ID = 126085
+MMU_ID = 126089
+IPAD_ID = 126093
+def default_image_transform(resolution=512):
+ return transforms.Compose([
+ transforms.Lambda(lambda img: img.convert('RGB')),
+ transforms.Resize(resolution, interpolation=transforms.InterpolationMode.BICUBIC),
+ transforms.CenterCrop(resolution),
+ transforms.ToTensor(), # [0,1]
+ transforms.Normalize(mean=[0.5]*3, std=[0.5]*3) # [-1,1]
+ ])
+
+def default_image_transform_pad(resolution=512, fill_color=(255, 255, 255)):
+ def _pad_and_resize(img):
+ img = img.convert('RGB')
+ w, h = img.size
+ if w == h:
+ padded_image = img
+ elif w < h:
+ padding_needed = h - w
+ padding_left = padding_needed // 2
+ padding_right = padding_needed - padding_left
+ pad_transform = transforms.Pad((padding_left, 0, padding_right, 0), fill=fill_color, padding_mode='constant')
+ padded_image = pad_transform(img)
+ else:
+ padding_needed = w - h
+ padding_top = padding_needed // 2
+ padding_bottom = padding_needed - padding_top
+ pad_transform = transforms.Pad((0, padding_top, 0, padding_bottom), fill=fill_color, padding_mode='constant')
+ padded_image = pad_transform(img)
+ return transforms.Resize((resolution, resolution), interpolation=transforms.InterpolationMode.BICUBIC)(padded_image)
+ return transforms.Compose([
+ transforms.Lambda(_pad_and_resize),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.5]*3, std=[0.5]*3)
+ ])
+
+@torch.no_grad()
+def encode_image_to_tokens(vq_model, image_pil, tokenizer, device, resolution=512, transform=None):
+ if transform is None:
+ transform = default_image_transform_pad()
+ img_tensor = transform(image_pil).unsqueeze(0).to(device) # [1,3,H,W]
+ codes = vq_model.get_code(img_tensor) # [1, L], int
+ offset = len(tokenizer)
+ image_token_ids = (codes + offset).long().squeeze(0) # [L]
+ return image_token_ids
+
+
+def get_num_transfer_tokens(mask_index, steps):
+ mask_num = mask_index.sum(dim=1, keepdim=True)
+
+ base = mask_num // steps
+ remainder = mask_num % steps
+
+ num_transfer_tokens = torch.zeros(mask_num.size(0), steps, device=mask_index.device, dtype=torch.int64) + base
+
+ for i in range(mask_num.size(0)):
+ num_transfer_tokens[i, :remainder[i]] += 1
+
+ return num_transfer_tokens
+
+
+
+# ──────────────────────────── return type ────────────────────────────────
+@dataclass
+class DiffusionOutput:
+ sequences: torch.Tensor # final result (B, L_total) (GPU)
+ history: List[torch.Tensor] # all intermediate x (CPU)
+ nfe: int
+
+
+
+
+
+
+@torch.no_grad()
+def generate_with_prefix_cache(
+ model, prompt,
+ steps, gen_length, block_length, temperature,
+ target, mask_id, further_horizon, use_cache, unmask_threshold
+ ) -> DiffusionOutput:
+
+ cgws = further_horizon
+ B, L0 = prompt.shape
+ x = torch.full((B, L0 + gen_length), mask_id, dtype=torch.long, device=prompt.device)
+ max_length = L0 + gen_length
+ x[:, :L0] = prompt
+ assert gen_length % block_length == 0
+ num_blocks = gen_length // block_length
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (i < rem) for i in range(num_blocks)]
+
+ nfe = 0
+ hist: List[torch.Tensor] = []
+
+ for blk in range(num_blocks):
+ s, e = L0 + blk * block_length, L0 + (blk + 1) * block_length
+
+ if cgws is not None:
+ window_end = max_length if cgws is None else min(e + cgws, max_length)
+ window_slice = slice(s, window_end)
+
+ cur_steps = steps_per_block[blk]
+ num_transfer = get_num_transfer_tokens((x[:, s:e] == mask_id), cur_steps)
+
+ # first full forward to build prefix cache
+ if use_cache:
+ out = model(x, use_cache=True)
+ pkv = out.past_key_values
+ # chop prefix out of past_kv to keep cache small
+ new_pkv = tuple(
+ tuple(t[:, :, :s] for t in layer) for layer in pkv
+ )
+ pkv = new_pkv
+ else:
+ out = model(x, use_cache=False)
+
+ mask_all = (x == mask_id)
+ mask_all[:, e:] = 0
+
+ x0, tr_idx = get_transfer_index(
+ out.logits, temperature, target, mask_all,
+ x, num_transfer[:, 0], unmask_threshold)
+ x[tr_idx] = x0[tr_idx]
+ hist.append(x.clone().cpu())
+ nfe += 1
+
+ i = 1
+ while i < cur_steps:
+ nfe += 1
+ if cgws is not None:
+ mask_blk = (x[:, window_slice] == mask_id)
+ else:
+ mask_blk = (x[:, s:] == mask_id)
+ mask_blk[:, block_length:] = 0
+
+ if use_cache:
+ if cgws is not None:
+ logits = model(x[:, window_slice], past_key_values=pkv, use_cache=True).logits
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, window_slice], num_transfer[:, i], unmask_threshold)
+ x[:, window_slice][tr_idx] = x0[tr_idx]
+ else:
+ logits = model(x[:, s:], past_key_values=pkv, use_cache=True).logits
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, s:], num_transfer[:, i], unmask_threshold)
+ x[:, s:][tr_idx] = x0[tr_idx]
+ else:
+ logits = model(x, use_cache=False).logits
+ logits = logits[:, s:]
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, s:], num_transfer[:, i], unmask_threshold)
+ x[:, s:][tr_idx] = x0[tr_idx]
+
+ hist.append(x.clone().cpu())
+
+ if (x[:, s:e] == mask_id).sum() == 0:
+ break
+ i += 1
+
+ return DiffusionOutput(sequences=x, history=hist, nfe=nfe)
+
+
+
+
+def get_transfer_index(logits, temperature, target, mask_index, x, num_transfer_tokens, threshold=None):
+ logits_with_noise = add_gumbel_noise(logits, temperature=temperature)
+ x0 = torch.argmax(logits_with_noise, dim=-1) # b, l
+
+ if target == 'confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = torch.squeeze(
+ torch.gather(p, dim=-1, index=torch.unsqueeze(x0, -1)), -1) # b, l
+ elif target == 'margin_confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ top2 = torch.topk(p, 2, dim=-1).values # (b, l, 2)
+ x0_p = top2[..., 0] - top2[..., 1] # Δ(top1, top2)
+ elif target == 'neg_entropy':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = -torch.sum(p * torch.log(p + 1e-10), dim=-1) # –entropy
+ elif target == 'random':
+ x0_p = torch.rand((x0.shape[0], x0.shape[1]), device=x0.device)
+ else:
+ raise NotImplementedError(target)
+
+ x0 = torch.where(mask_index, x0, x)
+
+ if threshold is not None:
+ selected = mask_index & (x0_p >= threshold) # (B, T)
+
+ has_mask = mask_index.any(dim=-1) # (B,)
+ none_sel = (~selected.any(dim=-1)) & has_mask # (B,)
+ if none_sel.any():
+ masked_scores = x0_p.masked_fill(~mask_index, float("-inf"))
+ best_idx = masked_scores.argmax(dim=-1) # (B,)
+ rows = torch.nonzero(none_sel, as_tuple=False).squeeze(-1)
+ selected[rows, best_idx[rows]] = True
+
+ return x0, selected
+
+ confidence = x0_p.masked_fill(~mask_index, float("-inf"))
+ transfer_index = torch.zeros_like(x0, dtype=torch.bool, device=x0.device)
+ for j in range(confidence.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+ _, sel = torch.topk(confidence[j], k=k)
+ transfer_index[j, sel] = True
+ return x0, transfer_index
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem=data_i["question"])
+
+
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+
+ for t, snap in enumerate(history, start=0):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ if (step_map == 0).sum() == 0:
+ break
+ return step_map
+
+
+
+from tqdm import tqdm
+
+def worker(pretrained_model, rank, prompts, orig_idx, data_idx, image_paths, seq_dict, step_dict, imgtok_dict, batch_size, config):
+ from PIL import Image
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+ # select model by base
+ model_gpu = (LLaDAModelLM
+ .from_pretrained(pretrained_model,
+ trust_remote_code=True,
+ torch_dtype=torch.bfloat16)
+ .to(device)
+ .eval())
+ tokenizer_gpu = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+ is_mmu = (config.model.model_base == "mmada" and (config.dataset.data_type == "mmu" or config.evaluation.data_type == "mmu"))
+ if is_mmu:
+ assert MAGVITv2 is not None, "MAGVITv2 is not installed. Please install or check PYTHONPATH."
+ vq_model = MAGVITv2.from_pretrained(config.model.vq_model_path).to(device).eval()
+ vq_model.requires_grad_(False)
+ def left_pad_batch(tensors, pad_id):
+ max_len = max(t.size(0) for t in tensors)
+ out = torch.full((len(tensors), max_len), pad_id, dtype=torch.long)
+ for i, t in enumerate(tensors):
+ out[i, -t.size(0):] = t
+ return out
+ mask_id = tokenizer_gpu.encode('<|mdm_mask|>')[0]
+ if config.rollout.use_cache == False:
+ config.rollout.further_horizon = None
+ unmask_threshold = None if config.rollout.remasking_strategy == "low_confidence_static" else config.rollout.dynamic_threshold
+ # local cache to avoid repeated VQ for the same sample within this worker
+ local_img_code_cache = {}
+ # process in chunks
+ for start in tqdm(range(0, len(prompts), batch_size),
+ desc=f"GPU {rank}", position=rank, leave=True):
+ batch_prompts = prompts[start:start+batch_size]
+ batch_idxs = orig_idx[start:start+batch_size]
+ batch_didx = data_idx[start:start+batch_size]
+ batch_ipaths = image_paths[start:start+batch_size] if image_paths is not None else None
+ if not is_mmu:
+ enc = tokenizer_gpu(batch_prompts, padding=True, return_tensors="pt")
+ input_ids = enc["input_ids"].to(device)
+ else:
+ mmu_inputs = []
+ for j, p in enumerate(batch_prompts):
+ did = batch_didx[j]
+ if did not in local_img_code_cache:
+ img_path = batch_ipaths[j]
+ if not os.path.isabs(img_path) and hasattr(config.dataset, "image_root") and config.dataset.image_root is not None:
+ img_path = os.path.join(config.dataset.image_root, img_path)
+ img = Image.open(img_path).convert("RGB")
+ img_codes = encode_image_to_tokens(vq_model, img, tokenizer_gpu, device, resolution=config.model.get("image_resolution", 512))
+ local_img_code_cache[did] = img_codes.detach().cpu()
+ imgtok_dict[did] = img_codes.detach().cpu().tolist()
+ img_codes = local_img_code_cache[did].to(device)
+ chat_ids = tokenizer_gpu([p], add_special_tokens=False)["input_ids"]
+ chat_ids = torch.tensor(chat_ids[0], dtype=torch.long, device=device) # [T_chat]
+ mmu = torch.tensor([MMU_ID, SOI_ID], dtype=torch.long, device=device) # [2]
+ eoi = torch.tensor([EOI_ID], dtype=torch.long, device=device) # [1]
+ full = torch.cat([mmu, img_codes, eoi, chat_ids], dim=0) # [T]
+ mmu_inputs.append(full)
+ pad_id = tokenizer_gpu.eos_token_id
+ input_ids = left_pad_batch(mmu_inputs, pad_id=pad_id).to(device)
+ out = generate_with_prefix_cache(
+ model_gpu, input_ids,
+ steps=config.rollout.steps, gen_length=config.rollout.max_gen_length,
+ block_length=config.rollout.block_size, temperature=config.rollout.temperature,
+ target=config.rollout.target, mask_id=mask_id, further_horizon=config.rollout.further_horizon,
+ use_cache=config.rollout.use_cache, unmask_threshold=unmask_threshold
+ )
+ out.sequences = out.sequences.cpu()
+ torch.cuda.empty_cache()
+ seq_ids = out.sequences[:, input_ids.shape[1]:].tolist()
+ texts = tokenizer_gpu.batch_decode(seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)
+ for i, idx in enumerate(batch_idxs):
+ m = denoise_step_map(out.history, mask_id=mask_id, sample_idx=i)
+ step_map = m[input_ids.shape[1]:].tolist()
+ seq_dict[idx] = texts[i]
+ step_dict[idx] = step_map
+ torch.cuda.empty_cache()
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+ mp.set_start_method("spawn", force=True)
+
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ project_name = config.experiment.project
+
+ if config.reward.answer_must_in_box:
+ assert config.dataset.add_think_prompt == False, "answer_must_in_box must be False when add_think_prompt is True"
+
+ if config.reward.answer_must_in_box:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+ elif config.dataset.add_think_prompt:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>\nYou should first think about the reasoning process in the mind and then provide the user with the answer. The reasoning process is enclosed within tags, i.e. reasoning process here answer here\n{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+ else:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+
+
+
+ code_task = False
+ if config.experiment.function == "train":
+ dataset = config.dataset.train_dataset
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ if config.dataset.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+
+ elif config.experiment.function == "evaluation":
+ dataset = config.evaluation.eval_dataset
+ if config.evaluation.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+ k_sample = config.evaluation.num_response_per_task
+ batch_size = config.evaluation.batch_size
+
+ config.rollout.steps = config.evaluation.steps
+ config.rollout.temperature = config.evaluation.temperature
+ config.rollout.target = config.evaluation.target
+ config.rollout.block_size = config.evaluation.block_size
+ config.rollout.use_cache = config.evaluation.use_cache
+ config.rollout.further_horizon = config.evaluation.further_horizon
+ config.rollout.remasking_strategy = config.evaluation.remasking_strategy
+ config.rollout.dynamic_threshold = config.evaluation.dynamic_threshold
+ config.rollout.target = config.evaluation.target
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+
+
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(8)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ if config.experiment.function == "train":
+ random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+
+ if config.experiment.function == "train":
+ random_select_num = config.rollout.num_task_per_step
+ random_select_num = int(random_select_num / num_node)
+ random_select_num = min(random_select_num, len(data))
+ data = random_select(data, random_select_num)
+ num = len(data)
+
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_task:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+ if config.model.model_base == "mmada" and (config.dataset.data_type == "mmu" or config.evaluation.data_type == "mmu"):
+ image_paths = []
+ for i in range(num):
+ # 每个样本重复 k_sample 次
+ image_paths += [data[i]["image"]] * k_sample
+ else:
+ image_paths = None
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+ # --------------------- 2. split to each GPU ----------------------
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1, "need >=2 GPUs for parallel inference"
+
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+ data_idx_full = [index_list[i] for i in shuffled_idx]
+ data_idx_chunks = split_even(data_idx_full, n_gpu)
+ if image_paths is not None:
+ image_paths_full = [image_paths[i] for i in shuffled_idx]
+ image_chunks = split_even(image_paths_full, n_gpu)
+ else:
+ image_chunks = [None] * n_gpu
+
+
+ # ------------------- 4. launch all workers -----------------------
+ manager = mp.Manager()
+ seq_dict = manager.dict() # {shuffled_pos: text}
+ step_dict = manager.dict() # {shuffled_pos: step_map}
+ imgtok_dict = manager.dict() # {data_idx: image_token_ids(list[int])}
+ procs = []
+
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ data_idx_chunks[rk],
+ image_chunks[rk],
+ seq_dict,
+ step_dict,
+ imgtok_dict,
+ batch_size,
+ config))
+ p.start()
+ procs.append(p)
+
+ for p in procs:
+ p.join()
+
+ # ------------------- 5. restore original order -------------------
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+ # 为每个 data[index] 写回 image_token_ids(仅MMU)
+ if (config.model.model_base == "mmada" and (config.dataset.data_type == "mmu" or config.evaluation.data_type == "mmu")):
+ for di in range(num):
+ if di in imgtok_dict:
+ data[di]["image_token_ids"] = imgtok_dict[di]
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import re
+
+ # calculate the response length (ignoring repeated <|endoftext|> tokens)
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_task:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ elif data[int(i/k_sample)]["test_method"] == "stdio":
+ extracted_output = extract_code(full_output)
+ else:
+ if config.reward.answer_must_in_box:
+ extracted_output = extract_final_boxed_answer(full_output)
+ else:
+ if "" in full_output:
+ extracted_output = full_output.split("")[1]
+ else:
+ extracted_output = full_output
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
diff --git a/sample/mmada_v_sample.py b/sample/mmada_v_sample.py
new file mode 100644
index 0000000000000000000000000000000000000000..27d1e3036ca45c0b154a181f027c91c5b53322d9
--- /dev/null
+++ b/sample/mmada_v_sample.py
@@ -0,0 +1,629 @@
+from __future__ import annotations
+import math, json, os, time
+from dataclasses import dataclass
+from typing import List, Tuple, Union
+import numpy as np
+from jinja2 import Template
+import torch
+from termcolor import cprint
+import torch.nn.functional as F
+from transformers import AutoTokenizer, AutoModel
+from llada.modeling_llada import LLaDAModelLM
+import multiprocessing as mp
+from torchvision import transforms
+from vq.modeling_magvitv2 import MAGVITv2
+
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+def add_gumbel_noise(logits, temperature):
+ if temperature == 0:
+ return logits
+ logits = logits.to(torch.float64)
+ noise = torch.rand_like(logits, dtype=torch.float64)
+ noise = (- torch.log(noise)) ** temperature
+ return logits.exp() / noise
+
+
+SOI_ID = 126084
+EOI_ID = 126085
+MMU_ID = 126089
+IPAD_ID = 126093
+
+def default_image_transform(resolution=512, fill_color=(255, 255, 255)):
+ def _pad_and_resize(img):
+ img = img.convert('RGB')
+ w, h = img.size
+ if w == h:
+ padded_image = img
+ elif w < h:
+ padding_needed = h - w
+ padding_left = padding_needed // 2
+ padding_right = padding_needed - padding_left
+ pad_transform = transforms.Pad((padding_left, 0, padding_right, 0), fill=fill_color, padding_mode='constant')
+ padded_image = pad_transform(img)
+ else:
+ padding_needed = w - h
+ padding_top = padding_needed // 2
+ padding_bottom = padding_needed - padding_top
+ pad_transform = transforms.Pad((0, padding_top, 0, padding_bottom), fill=fill_color, padding_mode='constant')
+ padded_image = pad_transform(img)
+ return transforms.Resize((resolution, resolution), interpolation=transforms.InterpolationMode.BICUBIC)(padded_image)
+ return transforms.Compose([
+ transforms.Lambda(_pad_and_resize),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.5]*3, std=[0.5]*3)
+ ])
+
+@torch.no_grad()
+def encode_image_to_tokens(vq_model, image_pil, tokenizer, device, resolution=512, transform=None):
+ if transform is None:
+ transform = default_image_transform(resolution)
+ img_tensor = transform(image_pil).unsqueeze(0).to(device) # [1,3,H,W]
+ codes = vq_model.get_code(img_tensor) # [1, L], int
+ offset = len(tokenizer)
+ image_token_ids = (codes + offset).long().squeeze(0) # [L]
+ return image_token_ids
+
+def get_num_transfer_tokens(mask_index, steps):
+ mask_num = mask_index.sum(dim=1, keepdim=True)
+
+ base = mask_num // steps
+ remainder = mask_num % steps
+
+ num_transfer_tokens = torch.zeros(mask_num.size(0), steps, device=mask_index.device, dtype=torch.int64) + base
+
+ for i in range(mask_num.size(0)):
+ num_transfer_tokens[i, :remainder[i]] += 1
+
+ return num_transfer_tokens
+
+
+
+
+# ──────────────────────────── return type ────────────────────────────────
+@dataclass
+class DiffusionOutput:
+ sequences: torch.Tensor # final result (B, L_total) (GPU)
+ history: List[torch.Tensor] # all intermediate x (CPU)
+ nfe: int
+
+
+
+
+
+
+@torch.no_grad()
+def generate_with_prefix_cache(
+ model, prompt,
+ steps, gen_length, block_length, temperature,
+ target, mask_id, further_horizon, use_cache, unmask_threshold
+ ) -> DiffusionOutput:
+
+ cgws = further_horizon
+ B, L0 = prompt.shape
+ x = torch.full((B, L0 + gen_length), mask_id, dtype=torch.long, device=prompt.device)
+ max_length = L0 + gen_length
+ x[:, :L0] = prompt
+ assert gen_length % block_length == 0
+ num_blocks = gen_length // block_length
+ base, rem = divmod(steps, num_blocks)
+ steps_per_block = [base + (i < rem) for i in range(num_blocks)]
+
+ nfe = 0
+ hist: List[torch.Tensor] = []
+
+ for blk in range(num_blocks):
+ s, e = L0 + blk * block_length, L0 + (blk + 1) * block_length
+
+ if cgws is not None:
+ window_end = max_length if cgws is None else min(e + cgws, max_length)
+ window_slice = slice(s, window_end)
+
+ cur_steps = steps_per_block[blk]
+ num_transfer = get_num_transfer_tokens((x[:, s:e] == mask_id), cur_steps)
+
+ # first full forward to build prefix cache
+ if use_cache:
+ out = model(x, use_cache=True)
+ pkv = out.past_key_values
+ # chop prefix out of past_kv to keep cache small
+ new_pkv = tuple(
+ tuple(t[:, :, :s] for t in layer) for layer in pkv
+ )
+ pkv = new_pkv
+ else:
+ out = model(x, use_cache=False)
+
+ mask_all = (x == mask_id)
+ mask_all[:, e:] = 0
+
+ x0, tr_idx = get_transfer_index(
+ out.logits, temperature, target, mask_all,
+ x, num_transfer[:, 0], unmask_threshold)
+ x[tr_idx] = x0[tr_idx]
+ hist.append(x.clone().cpu())
+ nfe += 1
+
+ i = 1
+ while True:
+ nfe += 1
+ if cgws is not None:
+ mask_blk = (x[:, window_slice] == mask_id)
+ else:
+ mask_blk = (x[:, s:] == mask_id)
+ mask_blk[:, block_length:] = 0
+
+ if use_cache:
+ if cgws is not None:
+ logits = model(x[:, window_slice], past_key_values=pkv, use_cache=True).logits
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, window_slice], num_transfer[:, i], unmask_threshold)
+ x[:, window_slice][tr_idx] = x0[tr_idx]
+ else:
+ logits = model(x[:, s:], past_key_values=pkv, use_cache=True).logits
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, s:], num_transfer[:, i], unmask_threshold)
+ x[:, s:][tr_idx] = x0[tr_idx]
+ else:
+ logits = model(x, use_cache=False).logits
+ logits = logits[:, s:]
+ x0, tr_idx = get_transfer_index(
+ logits, temperature, target,
+ mask_blk, x[:, s:], num_transfer[:, i], unmask_threshold)
+ x[:, s:][tr_idx] = x0[tr_idx]
+
+ hist.append(x.clone().cpu())
+
+ if (x[:, s:e] == mask_id).sum() == 0:
+ break
+ i += 1
+
+ return DiffusionOutput(sequences=x, history=hist, nfe=nfe)
+
+
+
+
+def get_transfer_index(logits, temperature, target, mask_index, x, num_transfer_tokens, threshold=None):
+ logits_with_noise = add_gumbel_noise(logits, temperature=temperature)
+ x0 = torch.argmax(logits_with_noise, dim=-1) # b, l
+
+ if target == 'confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = torch.squeeze(
+ torch.gather(p, dim=-1, index=torch.unsqueeze(x0, -1)), -1) # b, l
+ elif target == 'margin_confidence':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ top2 = torch.topk(p, 2, dim=-1).values # (b, l, 2)
+ x0_p = top2[..., 0] - top2[..., 1] # Δ(top1, top2)
+ elif target == 'neg_entropy':
+ p = F.softmax(logits.to(torch.float64), dim=-1)
+ x0_p = -torch.sum(p * torch.log(p + 1e-10), dim=-1) # –entropy
+ elif target == 'random':
+ x0_p = torch.rand((x0.shape[0], x0.shape[1]), device=x0.device)
+ else:
+ raise NotImplementedError(target)
+
+ x0 = torch.where(mask_index, x0, x)
+
+ if threshold is not None:
+ selected = mask_index & (x0_p >= threshold) # (B, T)
+
+ has_mask = mask_index.any(dim=-1) # (B,)
+ none_sel = (~selected.any(dim=-1)) & has_mask # (B,)
+ if none_sel.any():
+ masked_scores = x0_p.masked_fill(~mask_index, float("-inf"))
+ best_idx = masked_scores.argmax(dim=-1) # (B,)
+ rows = torch.nonzero(none_sel, as_tuple=False).squeeze(-1)
+ selected[rows, best_idx[rows]] = True
+
+ return x0, selected
+
+ confidence = x0_p.masked_fill(~mask_index, float("-inf"))
+ transfer_index = torch.zeros_like(x0, dtype=torch.bool, device=x0.device)
+ for j in range(confidence.shape[0]):
+ k = int(num_transfer_tokens[j].item() if torch.is_tensor(num_transfer_tokens[j]) else num_transfer_tokens[j])
+ if k <= 0:
+ continue
+ _, sel = torch.topk(confidence[j], k=k)
+ transfer_index[j, sel] = True
+ return x0, transfer_index
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+def denoise_step_map(history, mask_id: int, sample_idx: int = 0):
+ L = history[0].shape[1]
+ step_map = torch.zeros(L, dtype=torch.long)
+ prev = torch.full((L,), mask_id, dtype=torch.long)
+
+ for t, snap in enumerate(history, start=0):
+ cur = snap[sample_idx]
+ changed = (prev == mask_id) & (cur != mask_id)
+ step_map[changed] = t
+ prev = cur
+ if (step_map == 0).sum() == 0:
+ break
+ return step_map
+
+
+
+from tqdm import tqdm
+
+def worker(pretrained_model, rank, prompts, orig_idx, data_idx, image_paths, seq_dict, step_dict, batch_size, config):
+ from PIL import Image
+ torch.cuda.set_device(rank)
+ device = torch.device(f"cuda:{rank}")
+
+ model_gpu = (LLaDAModelLM
+ .from_pretrained(pretrained_model,
+ trust_remote_code=True,
+ torch_dtype=torch.bfloat16)
+ .to(device)
+ .eval())
+
+ tokenizer_gpu = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+ is_mmu = (config.model_base == "mmada" and config.dataset.data_type == "mmu")
+ if is_mmu:
+ assert MAGVITv2 is not None, "MAGVITv2 is not installed or not in PYTHONPATH."
+ vq_model = MAGVITv2.from_pretrained(config.vq_model_path).to(device).eval()
+ vq_model.requires_grad_(False)
+
+ def left_pad_batch(tensors, pad_id):
+ max_len = max(t.size(0) for t in tensors)
+ out = torch.full((len(tensors), max_len), pad_id, dtype=torch.long, device=device)
+ for i, t in enumerate(tensors):
+ out[i, -t.size(0):] = t.to(device)
+ return out
+
+ local_img_code_cache = {}
+
+ # process in chunks of `batch_size`
+ for start in tqdm(range(0, len(prompts), batch_size),
+ desc=f"GPU {rank}", position=rank, leave=True):
+ batch_prompts = prompts[start:start+batch_size]
+ batch_idxs = orig_idx[start:start+batch_size]
+ batch_didx = data_idx[start:start+batch_size] if data_idx is not None else None
+ batch_ipaths = image_paths[start:start+batch_size] if image_paths is not None else None
+
+ if not is_mmu:
+ enc = tokenizer_gpu(batch_prompts,
+ padding=True,
+ return_tensors="pt",
+ padding_side="left")
+ input_ids = enc["input_ids"].to(device)
+ else:
+ mmu_inputs = []
+ for j, p in enumerate(batch_prompts):
+ did = batch_didx[j]
+ if did not in local_img_code_cache:
+ img_path = batch_ipaths[j]
+ if not os.path.isabs(img_path) and hasattr(config.dataset, "image_root") and config.dataset.image_root is not None:
+ img_path = os.path.join(config.dataset.image_root, img_path)
+ img = Image.open(img_path).convert("RGB")
+ img_codes = encode_image_to_tokens(
+ vq_model, img, tokenizer_gpu, device,
+ resolution=getattr(config, "image_resolution", 512)
+ ).detach().cpu()
+ local_img_code_cache[did] = img_codes
+ img_codes = local_img_code_cache[did].to(device)
+
+ chat_ids = tokenizer_gpu([p], add_special_tokens=False)["input_ids"][0]
+ chat_ids = torch.tensor(chat_ids, dtype=torch.long, device=device)
+
+ mmu = torch.tensor([MMU_ID, SOI_ID], dtype=torch.long, device=device)
+ eoi = torch.tensor([EOI_ID], dtype=torch.long, device=device)
+
+ full = torch.cat([mmu, img_codes, eoi, chat_ids], dim=0)
+ mmu_inputs.append(full)
+
+ pad_id = tokenizer_gpu.eos_token_id
+ input_ids = left_pad_batch(mmu_inputs, pad_id=pad_id)
+
+ mask_id = tokenizer_gpu.encode('<|mdm_mask|>')[0]
+
+ if config.rollout.use_cache == False:
+ config.rollout.further_horizon = None
+
+ if config.rollout.remasking_strategy == "low_confidence_static":
+ unmask_threshold = None
+ else:
+ unmask_threshold = config.rollout.dynamic_threshold
+
+ out = generate_with_prefix_cache(
+ model_gpu, input_ids,
+ steps=config.rollout.steps, gen_length=config.rollout.max_gen_length,
+ block_length=config.rollout.block_size, temperature=config.rollout.temperature,
+ target=config.rollout.target, mask_id=mask_id, further_horizon=config.rollout.further_horizon,
+ use_cache=config.rollout.use_cache, unmask_threshold=unmask_threshold
+ )
+ out.sequences = out.sequences.cpu()
+ torch.cuda.empty_cache()
+
+ seq_ids = out.sequences[:, input_ids.shape[1]:].tolist()
+ texts = tokenizer_gpu.batch_decode(
+ seq_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True)
+
+ for i, idx in enumerate(batch_idxs):
+ m = denoise_step_map(out.history, mask_id=mask_id, sample_idx=i)
+ step_map = m[input_ids.shape[1]:].tolist()
+ seq_dict[idx] = texts[i]
+ step_dict[idx] = step_map
+
+ torch.cuda.empty_cache()
+
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+ mp.set_start_method("spawn", force=True)
+
+ if config.rollout.remasking_strategy == "low_confidence_static":
+ k_sample = 1
+ else:
+ k_sample = config.rollout.num_response_per_task
+ batch_size = config.rollout.batch_size
+
+ project_name = config.experiment.project
+
+ if config.answer_must_in_box:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>You need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+ else:
+ system_prompts = """<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}<|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n"""
+
+ code_eval = False
+
+ dataset = config.dataset.eval_dataset
+ pretrained_model = config.model
+ if config.dataset.data_type == "code":
+ code_eval = True
+ system_prompts_function = '''<|startoftext|><|start_header_id|>user<|end_header_id|>{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+ system_prompts_stdio = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\n You should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+ elif config.dataset.data_type == "option":
+ system_prompts = '''<|startoftext|><|start_header_id|>user<|end_header_id|>This is the problem:\n{{problem}}\nYou need to think step by step and put the final option (A, B, C, or D only—no other character) in \\boxed{}. <|eot_id|><|startoftext|><|start_header_id|>assistant<|end_header_id|>\n'''
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+ outputs_name = outputs_name + "-" + config.rollout.remasking_strategy
+
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(8)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ #random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+ num = len(data)
+
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True)
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_eval:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+
+
+ if config.model_base == "mmada" and config.dataset.data_type == "mmu":
+ image_paths = []
+ for i in range(num):
+ image_paths += [data[i]["image"]] * k_sample
+ else:
+ image_paths = None
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+ # --------------------- 2. split to each GPU ----------------------
+ n_gpu = torch.cuda.device_count()
+ assert n_gpu > 1, "need >=2 GPUs for parallel inference"
+
+ def split_even(lst, n):
+ k, m = divmod(len(lst), n)
+ return [lst[i*k+min(i,m):(i+1)*k+min(i+1,m)] for i in range(n)]
+
+ prompt_chunks = split_even(shuffled_prompts, n_gpu)
+ idx_chunks = split_even(shuffled_idx, n_gpu)
+ data_idx_full = [index_list[i] for i in shuffled_idx]
+ data_idx_chunks = split_even(data_idx_full, n_gpu)
+ if image_paths is not None:
+ image_paths_full = [image_paths[i] for i in shuffled_idx]
+ image_chunks = split_even(image_paths_full, n_gpu)
+ else:
+ image_chunks = [None] * n_gpu
+
+
+
+
+ # ------------------- 4. launch all workers -----------------------
+ manager = mp.Manager()
+ seq_dict = manager.dict() # {shuffled_pos: text}
+ step_dict = manager.dict() # {shuffled_pos: step_map}
+ procs = []
+
+ for rk in range(n_gpu):
+ p = mp.Process(target=worker,
+ args=(pretrained_model, rk,
+ prompt_chunks[rk],
+ idx_chunks[rk],
+ data_idx_chunks[rk],
+ image_chunks[rk],
+ seq_dict,
+ step_dict,
+ batch_size,
+ config))
+ p.start()
+ procs.append(p)
+
+
+ for p in procs:
+ p.join()
+
+ # ------------------- 5. restore original order -------------------
+ restored_outputs = [seq_dict[i] for i in range(N)]
+ restored_step_maps = [step_dict[i] for i in range(N)]
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import re
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_eval:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ else:
+ extracted_output = extract_code(full_output)
+ else:
+ if config.answer_must_in_box:
+ extracted_output = extract_final_boxed_answer(full_output)
+ else:
+ if "" in full_output:
+ extracted_output = full_output.split("")[1]
+ else:
+ extracted_output = full_output
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ data[index_i]["step_map"].append(restored_step_maps[i])
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
diff --git a/sample/sdar_rl_rollout.py b/sample/sdar_rl_rollout.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d2ad4bd079d2c231f706994a4a7c63df8b6c181
--- /dev/null
+++ b/sample/sdar_rl_rollout.py
@@ -0,0 +1,676 @@
+import os as _os
+_os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
+
+
+
+
+# Consolidate all caches into the local high-speed disk (NVMe or /dev/shm)
+# Local high-speed cache (NVMe or /dev/shm)
+_cache_root = "/dev/shm/torch_cache" # or "/local_nvme/torch_cache"
+_os.makedirs(_cache_root, exist_ok=True)
+_os.environ["TORCH_EXTENSIONS_DIR"] = _os.path.join(_cache_root, "torch_extensions")
+_os.environ["TRITON_CACHE_DIR"] = _os.path.join(_cache_root, "triton")
+_os.environ["XDG_CACHE_HOME"] = _cache_root
+_os.environ.setdefault("CUDA_MODULE_LOADING", "LAZY")
+
+
+
+_os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+_os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+_os.environ.pop("NCCL_BLOCKING_WAIT", None)
+_os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+
+import os
+import re
+import json
+from termcolor import cprint
+import random
+import torch.multiprocessing as mp
+from jinja2 import Template
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+import socket
+
+def _patch_safe_destroy():
+ import torch.distributed as dist
+ _real_destroy = dist.destroy_process_group
+ def _safe_destroy(group=None):
+ try:
+ if not dist.is_available():
+ return
+ try:
+ if not dist.is_initialized():
+ return
+ except Exception:
+ return
+ _real_destroy(group)
+ except AssertionError:
+ pass
+ dist.destroy_process_group = _safe_destroy
+
+
+
+def _llm_worker_run(args):
+ (model_path, tp, block_size, sampling_kwargs, vis_ids,
+ prompts_slice, indices_slice, enforce_eager, max_active, store_port) = args
+
+ import os
+ # 1) Setup environment (critical for correct worker behavior)
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.pop("NCCL_BLOCKING_WAIT", None)
+ os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, vis_ids))
+ #os.environ.setdefault("TORCH_EXTENSIONS_DIR", f"/tmp/torch_ext_worker_{store_port}")
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(store_port)
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
+
+ # 1.1) Create a per-worker `sitecustomize.py` and inject it into PYTHONPATH
+ # (this must be done before importing torch/jetengine)
+ patch_dir = f"/tmp/je_site_{store_port}"
+ os.makedirs(patch_dir, exist_ok=True)
+ patch_file = os.path.join(patch_dir, "sitecustomize.py")
+ # Important: the content must start at column 0 (no indentation)!
+ with open(patch_file, "w") as _f:
+ _f.write(
+ "import os\n"
+ "import torch.distributed as dist\n"
+ "_real = dist.init_process_group\n"
+ "def _wrapped(backend, init_method=None, *args, **kwargs):\n"
+ " port = os.environ.get('JE_TCP_PORT')\n"
+ " if port and isinstance(init_method, str) and init_method.startswith('tcp://localhost:2333'):\n"
+ " init_method = f'tcp://127.0.0.1:{port}'\n"
+ " return _real(backend, init_method, *args, **kwargs)\n"
+ "dist.init_process_group = _wrapped\n"
+ )
+ os.environ["PYTHONPATH"] = patch_dir + (":" + os.environ["PYTHONPATH"] if "PYTHONPATH" in os.environ else "")
+ os.environ["JE_TCP_PORT"] = str(store_port)
+
+ # 2) Import torch and patch the current worker process
+ import torch
+ import torch.distributed as dist
+ _patch_dist_port(store_port) # Patch port binding for this process
+ _patch_safe_destroy() # Avoid AssertionError in destroy_process_group
+ torch.cuda.set_device(0) # From this worker’s perspective, cuda:0 is the first visible device
+
+ # For debugging: print the worker’s CUDA_VISIBLE_DEVICES and assigned port
+ print(f"[worker pid={os.getpid()}] CVD={os.environ['CUDA_VISIBLE_DEVICES']}, port={store_port}, prompts={len(prompts_slice)}", flush=True)
+
+ # 3) Import jetengine and create the engine
+ # (child processes inherit the sitecustomize patch)
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=tp,
+ mask_token_id=151669,
+ block_length=block_size
+ )
+ sp = SamplingParams(**sampling_kwargs)
+ outs = llm.generate_streaming(prompts_slice, sp, max_active=max_active)
+ #seq = [o["text"] for o in outs]
+ #print(outs[0]["first_unmask_times"])
+ triples = []
+ for j, o in enumerate(outs):
+ triples.append((
+ indices_slice[j], # Global index (used to restore original order)
+ o["text"], # Generated text
+ o.get("first_unmask_times", None) # Optional time series aligned with completion tokens
+ ))
+
+ try:
+ if hasattr(llm, "shutdown"):
+ llm.shutdown()
+ except Exception:
+ pass
+
+ return triples
+
+
+
+def _llm_worker_entry(args, out_q):
+ import traceback, os
+ try:
+ res = _llm_worker_run(args)
+ out_q.put(("ok", res))
+ except Exception:
+ out_q.put(("err", {
+ "pid": os.getpid(),
+ "port": args[-1], # store_port
+ "traceback": traceback.format_exc(),
+ }))
+
+
+def _find_free_port():
+ s = socket.socket(); s.bind(('', 0))
+ p = s.getsockname()[1]; s.close()
+ return p
+
+def _patch_dist_port(port: int):
+ import torch.distributed as _dist
+ _real_init = _dist.init_process_group
+
+ def _wrapped(backend, init_method=None, *args, **kwargs):
+ # jetengine internally hardcodes "tcp://localhost:2333" — replace the port here
+ if isinstance(init_method, str) and init_method.startswith("tcp://localhost:2333"):
+ init_method = f"tcp://127.0.0.1:{port}"
+ return _real_init(backend, init_method, *args, **kwargs)
+
+ _dist.init_process_group = _wrapped
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+if __name__ == "__main__":
+
+
+
+ tp = int(get_config().rollout.tensor_parallel_size) # Or check after loading config
+
+ if tp == 1:
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ # These two are NCCL’s own variables, keep using the NCCL_ prefix
+ os.environ.setdefault("NCCL_P2P_DISABLE", "1")
+ os.environ.setdefault("NCCL_IB_DISABLE", "1")
+ else:
+ # For multi-GPU communication, do not disable P2P/IB;
+ # also clean up related variables (both old and new names)
+ for k in [
+ "NCCL_P2P_DISABLE", "NCCL_IB_DISABLE",
+ "TORCH_NCCL_BLOCKING_WAIT", "TORCH_NCCL_ASYNC_ERROR_HANDLING",
+ "NCCL_BLOCKING_WAIT", "NCCL_ASYNC_ERROR_HANDLING",
+ ]:
+ os.environ.pop(k, None)
+
+
+
+ from transformers import AutoTokenizer
+
+ # --- graceful shutdown & unique port ---
+ import os, sys, atexit, signal, torch.distributed as dist
+
+
+ # 2) Automatically set compile architecture according to the local GPU
+ # (do NOT hardcode 8.0)
+ def _set_arch():
+ try:
+ if torch.cuda.is_available():
+ major, minor = torch.cuda.get_device_capability(0)
+ os.environ["TORCH_CUDA_ARCH_LIST"] = f"{major}.{minor}"
+ except Exception:
+ pass
+ _set_arch()
+
+
+
+ # 1) Use a new port at each startup to avoid conflicts with 2333
+ if "MASTER_PORT" not in os.environ:
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(_find_free_port())
+ # (If JetEngine hardcodes tcp://localhost:2333 instead of using env://,
+ # see the “special case” section at the end)
+
+ # 2) Intercept Ctrl-C/TERM to destroy distributed groups & engine gracefully
+ _llm = None
+ _child_ps = [] # If you create your own mp.Process/Pool, append objects here
+
+ def _cleanup():
+ # 2.1) Shutdown JetEngine engine (if API available)
+ global _llm
+ try:
+ if _llm is not None and hasattr(_llm, "shutdown"):
+ _llm.shutdown()
+ except Exception:
+ pass
+ # 2.3) Kill/join child processes
+ for p in _child_ps:
+ try:
+ if hasattr(p, "terminate"): p.terminate()
+ except Exception:
+ pass
+ for p in _child_ps:
+ try:
+ if hasattr(p, "join"): p.join(timeout=2)
+ except Exception:
+ pass
+
+ atexit.register(_cleanup)
+ def _sig_handler(sig, frame):
+ _cleanup()
+ # 130: standard exit code for SIGINT; 143: for SIGTERM
+ sys.exit(130 if sig == signal.SIGINT else 143)
+
+ signal.signal(signal.SIGINT, _sig_handler)
+ signal.signal(signal.SIGTERM, _sig_handler)
+
+
+
+
+
+
+
+
+
+ config = get_config()
+
+ try:
+ if mp.get_start_method(allow_none=True) != "spawn":
+ mp.set_start_method("spawn", force=True)
+ except RuntimeError:
+ pass
+
+
+
+ system_prompts = '''<|im_start|>user\n{{problem}}\nPlease reason step by step, and put your final answer within \\boxed{}.<|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts = '''<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+
+
+ project_name = config.experiment.project
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+ code_task = False
+ if config.experiment.function == "train":
+ dataset = config.dataset.train_dataset
+ k_sample = config.rollout.num_response_per_task
+
+ if config.dataset.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+ elif config.experiment.function == "evaluation":
+ dataset = config.evaluation.eval_dataset
+ if config.evaluation.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+
+ k_sample = config.evaluation.num_response_per_task
+
+ config.rollout.tensor_parallel_size = config.evaluation.tensor_parallel_size
+ config.rollout.max_active = config.evaluation.max_active
+ config.rollout.max_token = config.evaluation.max_token
+ config.rollout.remasking_strategy = config.evaluation.remasking_strategy
+ config.rollout.dynamic_threshold = config.evaluation.dynamic_threshold
+ config.rollout.denoising_steps_per_block = config.evaluation.denoising_steps_per_block
+ config.rollout.temperature = config.evaluation.temperature
+ config.rollout.top_p = config.evaluation.top_p
+ config.rollout.top_k = config.evaluation.top_k
+ config.rollout.block_size = config.evaluation.block_size
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+
+
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(8)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ if config.experiment.function == "train":
+ random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+
+ if config.experiment.function == "train":
+ random_select_num = config.rollout.num_task_per_step
+ random_select_num = int(random_select_num / num_node)
+ random_select_num = min(random_select_num, len(data))
+ data = random_select(data, random_select_num)
+ num = len(data)
+
+ model_path = os.path.expanduser(pretrained_model)
+ tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
+ # Initialize the LLM
+
+ block_size = config.rollout.block_size
+
+
+
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_task:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+
+ import torch, math
+ print(f"[preflight] CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES')}")
+ print(f"[preflight] parent sees torch.cuda.device_count()={torch.cuda.device_count()}")
+
+ cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
+ if cvd:
+ visible_gpus = [x.strip() for x in cvd.split(",") if x.strip() != ""]
+ device_ids = [int(x) for x in visible_gpus]
+ else:
+ device_ids = list(range(torch.cuda.device_count()))
+
+ gpu_num = len(device_ids)
+ tp = int(config.rollout.tensor_parallel_size)
+ assert gpu_num >= tp, f"Visible GPUs ({gpu_num}) < tensor_parallel_size ({tp})."
+ assert gpu_num >= 1, "No GPU visible"
+ if tp > 1:
+ ngroups = 1
+ else:
+ ngroups = max(1, gpu_num // max(1, tp))
+
+ groups = [ device_ids[i*tp : (i+1)*tp] for i in range(ngroups) ]
+
+ sampling_kwargs = dict(
+ temperature = config.rollout.temperature,
+ topk = config.rollout.top_k,
+ topp = config.rollout.top_p,
+ max_tokens = config.rollout.max_token,
+ remasking_strategy = config.rollout.remasking_strategy,
+ block_length = block_size,
+ denoising_steps = config.rollout.denoising_steps_per_block,
+ dynamic_threshold = config.rollout.dynamic_threshold
+ )
+ max_active_local = config.rollout.max_active
+
+ def _chunk_by_groups(lst, ng):
+ L = len(lst)
+ if ng <= 1: return [lst]
+ chunk_size = math.ceil(L / ng)
+ return [ lst[i*chunk_size : min((i+1)*chunk_size, L)] for i in range(ng) ]
+
+ prompt_chunks = _chunk_by_groups(shuffled_prompts, ngroups)
+ index_chunks = _chunk_by_groups(shuffled_idx, ngroups)
+
+ for a, b in zip(prompt_chunks, index_chunks):
+ assert len(a) == len(b)
+
+ seq_pairs = []
+
+ if ngroups == 1:
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, groups[0]))
+ import torch
+ torch.cuda.set_device(0)
+
+ if config.rollout.tensor_parallel_size > 1:
+ enforce_eager = False
+ else:
+ enforce_eager = True
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=config.rollout.tensor_parallel_size,
+ mask_token_id=151669, # Optional: only needed for masked/diffusion models
+ block_length=block_size
+ )
+ _llm = llm
+
+ # Set sampling/generation parameters
+ sampling_params = SamplingParams(
+ temperature=config.rollout.temperature,
+ topk=config.rollout.top_k,
+ topp=config.rollout.top_p,
+ max_tokens=config.rollout.max_token,
+ remasking_strategy=config.rollout.remasking_strategy,
+ block_length=block_size,
+ denoising_steps=config.rollout.denoising_steps_per_block,
+ dynamic_threshold=config.rollout.dynamic_threshold
+ )
+ try:
+ outputs = llm.generate_streaming(prompt_chunks[0], sampling_params, max_active=config.rollout.max_active)
+ for j, o in enumerate(outputs):
+ seq_pairs.append( (
+ index_chunks[0][j],
+ o["text"],
+ o.get("first_unmask_times", None)
+ ) )
+ finally:
+ _cleanup()
+ else:
+ ctx = mp.get_context("spawn")
+ enforce_eager_local = False if tp > 1 else True
+
+ base_port = 29000
+ store_ports = [base_port + g for g in range(ngroups)]
+
+ out_q = ctx.Queue()
+ procs = []
+ for g in range(ngroups):
+ if len(prompt_chunks[g]) == 0:
+ continue
+ args = (
+ model_path, tp, block_size, sampling_kwargs, groups[g],
+ prompt_chunks[g], index_chunks[g],
+ enforce_eager_local, max_active_local, store_ports[g],
+ )
+ p = ctx.Process(target=_llm_worker_entry, args=(args, out_q), daemon=False)
+ p.start()
+ procs.append(p)
+ _child_ps.append(p)
+
+ import queue, time
+
+ results_needed = len(procs)
+ results_got = 0
+
+ while results_got < results_needed:
+ try:
+ kind, payload = out_q.get(timeout=1800)
+ except queue.Empty:
+ dead = [p for p in procs if not p.is_alive()]
+ if dead:
+ for p in dead:
+ print(f"[parent] worker pid={p.pid} exitcode={p.exitcode} (no result)", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Some workers died without returning results. See logs above.")
+ continue
+
+ if kind == "ok":
+ seq_pairs.extend(payload)
+ results_got += 1
+ else: # "err"
+ print(f"[parent] worker error on port {payload['port']} pid {payload['pid']}:\n{payload['traceback']}", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Worker failed. See traceback above.")
+
+ for p in procs:
+ p.join()
+
+
+ # ------------------- 3. restore original order -------------------
+
+
+ restored_outputs = [None] * N
+ restored_steps = [None] * N
+
+ for item in seq_pairs:
+ if len(item) == 2:
+ gi, text = item
+ steps = None
+ else:
+ gi, text, steps = item
+ restored_outputs[gi] = text
+ restored_steps[gi] = steps
+
+
+ for i in range(N):
+ if restored_outputs[i] is None:
+ restored_outputs[i] = ""
+ if restored_steps[i] is None:
+ restored_steps[i] = ""
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_task:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ else:
+ extracted_output = extract_code(full_output)
+ else:
+ extracted_output = extract_final_boxed_answer(full_output)
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ step_map_i = restored_steps[i] if restored_steps[i] is not None else []
+ #print(step_map_i)
+ data[index_i]["step_map"].append(step_map_i)
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+
+
diff --git a/sample/sdar_sample.py b/sample/sdar_sample.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e6ee6115ab5542d071c2d28eed8ecd74edb0281
--- /dev/null
+++ b/sample/sdar_sample.py
@@ -0,0 +1,693 @@
+import os as _os
+_os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
+
+
+
+# Consolidate all caches into the local high-speed disk (NVMe or /dev/shm)
+# Local high-speed cache (NVMe or /dev/shm)
+_cache_root = "/dev/shm/torch_cache" # or "/local_nvme/torch_cache"
+_os.makedirs(_cache_root, exist_ok=True)
+_os.environ["TORCH_EXTENSIONS_DIR"] = _os.path.join(_cache_root, "torch_extensions")
+_os.environ["TRITON_CACHE_DIR"] = _os.path.join(_cache_root, "triton")
+_os.environ["XDG_CACHE_HOME"] = _cache_root
+_os.environ.setdefault("CUDA_MODULE_LOADING", "LAZY")
+
+
+_os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+_os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+_os.environ.pop("NCCL_BLOCKING_WAIT", None)
+_os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+
+import os
+import re
+import json
+from termcolor import cprint
+import random
+import torch.multiprocessing as mp
+from jinja2 import Template
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+'''
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+'''
+def get_data_chunk(data, num_nodes, node_idx):
+ total = len(data)
+ start = (total * node_idx) // num_nodes
+ end = (total * (node_idx + 1)) // num_nodes
+ return data[start:end]
+
+
+import socket
+
+def _patch_safe_destroy():
+ import torch.distributed as dist
+ _real_destroy = dist.destroy_process_group
+ def _safe_destroy(group=None):
+ try:
+ if not dist.is_available():
+ return
+ try:
+ if not dist.is_initialized():
+ return
+ except Exception:
+ return
+ _real_destroy(group)
+ except AssertionError:
+ pass
+ dist.destroy_process_group = _safe_destroy
+
+
+
+def _llm_worker_run(args):
+ (model_path, tp, block_size, sampling_kwargs, vis_ids,
+ prompts_slice, indices_slice, enforce_eager, max_active, store_port) = args
+
+ import os
+ # 1) Setup environment (critical for correct worker behavior)
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.pop("NCCL_BLOCKING_WAIT", None)
+ os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, vis_ids))
+ #os.environ.setdefault("TORCH_EXTENSIONS_DIR", f"/tmp/torch_ext_worker_{store_port}")
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(store_port)
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
+
+ # 1.1) Create a per-worker `sitecustomize.py` and inject it into PYTHONPATH
+ # (this must be done before importing torch/jetengine)
+ patch_dir = f"/tmp/je_site_{store_port}"
+ os.makedirs(patch_dir, exist_ok=True)
+ patch_file = os.path.join(patch_dir, "sitecustomize.py")
+ # Important: the content must start at column 0 (no indentation)!
+ with open(patch_file, "w") as _f:
+ _f.write(
+ "import os\n"
+ "import torch.distributed as dist\n"
+ "_real = dist.init_process_group\n"
+ "def _wrapped(backend, init_method=None, *args, **kwargs):\n"
+ " port = os.environ.get('JE_TCP_PORT')\n"
+ " if port and isinstance(init_method, str) and init_method.startswith('tcp://localhost:2333'):\n"
+ " init_method = f'tcp://127.0.0.1:{port}'\n"
+ " return _real(backend, init_method, *args, **kwargs)\n"
+ "dist.init_process_group = _wrapped\n"
+ )
+ os.environ["PYTHONPATH"] = patch_dir + (":" + os.environ["PYTHONPATH"] if "PYTHONPATH" in os.environ else "")
+ os.environ["JE_TCP_PORT"] = str(store_port)
+
+ # 2) Import torch and patch the current worker process
+ import torch
+ import torch.distributed as dist
+ _patch_dist_port(store_port) # Patch port binding for this process
+ _patch_safe_destroy() # Avoid AssertionError in destroy_process_group
+ torch.cuda.set_device(0) # From this worker’s perspective, cuda:0 is the first visible device
+
+ # For debugging: print the worker’s CUDA_VISIBLE_DEVICES and assigned port
+ print(f"[worker pid={os.getpid()}] CVD={os.environ['CUDA_VISIBLE_DEVICES']}, port={store_port}, prompts={len(prompts_slice)}", flush=True)
+
+ # 3) Import jetengine and create the engine
+ # (child processes inherit the sitecustomize patch)
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ llm = None
+ triples = []
+ try:
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=tp,
+ mask_token_id=151669,
+ block_length=block_size
+ )
+ sp = SamplingParams(**sampling_kwargs)
+
+ # Keep max_active sane for each worker’s slice to avoid rare internal exits
+ local_max_active = min(max_active, max(1, len(prompts_slice)))
+ outs = llm.generate_streaming(prompts_slice, sp, max_active=local_max_active)
+
+ # Collect results incrementally so we can return partials on any exit
+ for j, o in enumerate(outs):
+ triples.append((
+ indices_slice[j],
+ o["text"],
+ o.get("first_unmask_times", None)
+ ))
+ except BaseException as e:
+ # Swallow SystemExit/KeyboardInterrupt/etc. so we can return partials
+ print(f"[worker pid={os.getpid()}] Caught {type(e).__name__}: {e}. Returning partial results ({len(triples)})", flush=True)
+ finally:
+ try:
+ if llm is not None and hasattr(llm, "shutdown"):
+ llm.shutdown()
+ except Exception:
+ pass
+
+ return triples
+
+
+
+def _llm_worker_entry(args, out_q):
+ import traceback, os
+ try:
+ res = _llm_worker_run(args)
+ # Even if partial, report as 'ok' so parent can use what we have
+ out_q.put(("ok", res))
+ except BaseException:
+ tb = traceback.format_exc()
+ # Fall back to 'err' path if even the call above exploded
+ try:
+ out_q.put(("err", {
+ "pid": os.getpid(),
+ "port": args[-1],
+ "traceback": tb,
+ }))
+ except Exception:
+ pass
+
+
+def _find_free_port():
+ s = socket.socket(); s.bind(('', 0))
+ p = s.getsockname()[1]; s.close()
+ return p
+
+def _patch_dist_port(port: int):
+ import torch.distributed as _dist
+ _real_init = _dist.init_process_group
+
+ def _wrapped(backend, init_method=None, *args, **kwargs):
+ # jetengine internally hardcodes "tcp://localhost:2333" — replace the port here
+ if isinstance(init_method, str) and init_method.startswith("tcp://localhost:2333"):
+ init_method = f"tcp://127.0.0.1:{port}"
+ return _real_init(backend, init_method, *args, **kwargs)
+
+ _dist.init_process_group = _wrapped
+
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+
+
+ tp = int(get_config().rollout.tensor_parallel_size) # Or check after loading config
+
+ if tp == 1:
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ # These two are NCCL’s own variables, keep using the NCCL_ prefix
+ os.environ.setdefault("NCCL_P2P_DISABLE", "1")
+ os.environ.setdefault("NCCL_IB_DISABLE", "1")
+ else:
+ # For multi-GPU communication, do not disable P2P/IB;
+ # also clean up related variables (both old and new names)
+ for k in [
+ "NCCL_P2P_DISABLE", "NCCL_IB_DISABLE",
+ "TORCH_NCCL_BLOCKING_WAIT", "TORCH_NCCL_ASYNC_ERROR_HANDLING",
+ "NCCL_BLOCKING_WAIT", "NCCL_ASYNC_ERROR_HANDLING",
+ ]:
+ os.environ.pop(k, None)
+
+
+
+ from transformers import AutoTokenizer
+
+ # --- graceful shutdown & unique port ---
+ import os, sys, atexit, signal, torch.distributed as dist
+
+ # 2) Automatically set compile architecture according to the local GPU
+ # (do NOT hardcode 8.0)
+ def _set_arch():
+ try:
+ if torch.cuda.is_available():
+ major, minor = torch.cuda.get_device_capability(0)
+ os.environ["TORCH_CUDA_ARCH_LIST"] = f"{major}.{minor}"
+ except Exception:
+ pass
+ _set_arch()
+
+
+
+ # 1) Use a new port at each startup to avoid conflicts with 2333
+ if "MASTER_PORT" not in os.environ:
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(_find_free_port())
+ # (If JetEngine hardcodes tcp://localhost:2333 instead of using env://,
+ # see the “special case” section at the end)
+
+ # 2) Intercept Ctrl-C/TERM to destroy distributed groups & engine gracefully
+ _llm = None
+ _child_ps = [] # If you create your own mp.Process/Pool, append objects here
+
+ def _cleanup():
+ # 2.1) Shutdown JetEngine engine (if API available)
+ global _llm
+ try:
+ if _llm is not None and hasattr(_llm, "shutdown"):
+ _llm.shutdown()
+ except Exception:
+ pass
+ # 2.3) Kill/join child processes
+ for p in _child_ps:
+ try:
+ if hasattr(p, "terminate"): p.terminate()
+ except Exception:
+ pass
+ for p in _child_ps:
+ try:
+ if hasattr(p, "join"): p.join(timeout=2)
+ except Exception:
+ pass
+
+ atexit.register(_cleanup)
+ def _sig_handler(sig, frame):
+ _cleanup()
+ # 130: standard exit code for SIGINT; 143: for SIGTERM
+ sys.exit(130 if sig == signal.SIGINT else 143)
+
+ signal.signal(signal.SIGINT, _sig_handler)
+ signal.signal(signal.SIGTERM, _sig_handler)
+
+
+
+
+
+
+
+
+ try:
+ if mp.get_start_method(allow_none=True) != "spawn":
+ mp.set_start_method("spawn", force=True)
+ except RuntimeError:
+ pass
+
+
+ k_sample = config.rollout.num_response_per_task
+ # non-cot prompt
+ #system_prompts = '''<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+ # cot prompt
+ system_prompts = '''<|im_start|>user\n{{problem}}\nPlease reason step by step, and put your final answer within \\boxed{}.<|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts = '''<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+
+ project_name = config.experiment.project
+
+ code_eval = False
+
+ dataset = config.dataset.eval_dataset
+ pretrained_model = config.model
+ if config.dataset.data_type == "code":
+ code_eval = True
+ system_prompts_function = '''<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ elif config.dataset.data_type == "option":
+ system_prompts = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou need to think step by step and put the final option (A, B, C, or D only—no other character) in \\boxed{}. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou need to think step by step and put the final option (A, B, C, or D only—no other character) in \\boxed{}. <|im_end|>\n<|im_start|>assistant\n'''
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(100, 300)]
+ #data = data[400:]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ #random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+ num = len(data)
+
+
+ model_path = os.path.expanduser(pretrained_model)
+ tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
+ # Initialize the LLM
+
+ block_size = config.rollout.block_size
+
+
+
+
+
+
+
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_eval:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+
+ import torch, math
+ print(f"[preflight] CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES')}")
+ print(f"[preflight] parent sees torch.cuda.device_count()={torch.cuda.device_count()}")
+
+ cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
+ if cvd:
+ visible_gpus = [x.strip() for x in cvd.split(",") if x.strip() != ""]
+ device_ids = [int(x) for x in visible_gpus]
+ else:
+ device_ids = list(range(torch.cuda.device_count()))
+
+ gpu_num = len(device_ids)
+ tp = int(config.rollout.tensor_parallel_size)
+ assert gpu_num >= tp, f"Visible GPUs ({gpu_num}) < tensor_parallel_size ({tp})."
+ assert gpu_num >= 1, "No GPU visible"
+ if tp > 1:
+ ngroups = 1
+ else:
+ ngroups = max(1, gpu_num // max(1, tp))
+
+ groups = [ device_ids[i*tp : (i+1)*tp] for i in range(ngroups) ]
+
+
+
+ def to_single_token_stop_ids(tokenizer, stop_token_list):
+ if not stop_token_list:
+ return []
+ ids, seen = [], set()
+ for s in stop_token_list:
+ if isinstance(s, int):
+ tid = [s]
+ elif isinstance(s, str):
+ tid = tokenizer.encode(s, add_special_tokens=False)
+ elif isinstance(s, (list, tuple)) and all(isinstance(x, int) for x in s):
+ tid = list(s)
+ else:
+ continue
+ if len(tid) == 1:
+ t = tid[0]
+ if t not in seen:
+ seen.add(t)
+ ids.append(t)
+ return ids
+
+ from omegaconf import MISSING
+ if OmegaConf.select(config, "rollout.stop_token_list", default=MISSING) is not MISSING:
+ stop_token_id_list = to_single_token_stop_ids(tokenizer, config.rollout.stop_token_list)
+ else:
+ stop_token_id_list = []
+
+ sampling_kwargs = dict(
+ temperature = config.rollout.temperature,
+ topk = config.rollout.top_k,
+ topp = config.rollout.top_p,
+ max_tokens = config.rollout.max_token,
+ remasking_strategy = config.rollout.remasking_strategy,
+ block_length = block_size,
+ denoising_steps = config.rollout.denoising_steps_per_block,
+ dynamic_threshold = config.rollout.dynamic_threshold,
+ stop_words = stop_token_id_list
+ )
+ max_active_local = config.rollout.max_active
+
+ def _chunk_by_groups(lst, ng):
+ L = len(lst)
+ if ng <= 1: return [lst]
+ chunk_size = math.ceil(L / ng)
+ return [ lst[i*chunk_size : min((i+1)*chunk_size, L)] for i in range(ng) ]
+
+ prompt_chunks = _chunk_by_groups(shuffled_prompts, ngroups)
+ index_chunks = _chunk_by_groups(shuffled_idx, ngroups)
+
+ for a, b in zip(prompt_chunks, index_chunks):
+ assert len(a) == len(b)
+
+ seq_pairs = []
+
+ if ngroups == 1:
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, groups[0]))
+ import torch
+ torch.cuda.set_device(0)
+
+ if config.rollout.tensor_parallel_size > 1:
+ enforce_eager = False
+ else:
+ enforce_eager = True
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=config.rollout.tensor_parallel_size,
+ mask_token_id=151669, # Optional: only needed for masked/diffusion models
+ block_length=block_size
+ )
+ _llm = llm
+
+ # Set sampling/generation parameters
+ sampling_params = SamplingParams(
+ temperature=config.rollout.temperature,
+ topk=config.rollout.top_k,
+ topp=config.rollout.top_p,
+ max_tokens=config.rollout.max_token,
+ remasking_strategy=config.rollout.remasking_strategy,
+ block_length=block_size,
+ denoising_steps=config.rollout.denoising_steps_per_block,
+ dynamic_threshold=config.rollout.dynamic_threshold,
+ stop_words = stop_token_id_list
+ )
+ try:
+ outputs = llm.generate_streaming(prompt_chunks[0], sampling_params, max_active=config.rollout.max_active)
+ for j, o in enumerate(outputs):
+ seq_pairs.append( (
+ index_chunks[0][j],
+ o["text"],
+ o.get("first_unmask_times", None)
+ ) )
+ finally:
+ _cleanup()
+ else:
+ import time
+ ctx = mp.get_context("spawn")
+ enforce_eager_local = False if tp > 1 else True
+
+ base_port = 29000
+ store_ports = [base_port + g for g in range(ngroups)]
+
+ out_q = ctx.Queue()
+ procs = []
+ for g in range(ngroups):
+ if len(prompt_chunks[g]) == 0:
+ continue
+ args = (
+ model_path, tp, block_size, sampling_kwargs, groups[g],
+ prompt_chunks[g], index_chunks[g],
+ enforce_eager_local, max_active_local, store_ports[g],
+ )
+ p = ctx.Process(target=_llm_worker_entry, args=(args, out_q), daemon=False)
+ p.start()
+ #time.sleep(2)
+ procs.append(p)
+ _child_ps.append(p)
+
+ import queue, time
+
+ results_needed = len(procs)
+ results_got = 0
+
+ while results_got < results_needed:
+ try:
+ kind, payload = out_q.get(timeout=3600 * 24)
+ except queue.Empty:
+ dead = [p for p in procs if not p.is_alive()]
+ if dead:
+ for p in dead:
+ print(f"[parent] worker pid={p.pid} exitcode={p.exitcode} (no result)", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Some workers died without returning results. See logs above.")
+ continue
+
+ if kind == "ok":
+ seq_pairs.extend(payload)
+ results_got += 1
+ else: # "err"
+ print(f"[parent] worker error on port {payload['port']} pid {payload['pid']}:\n{payload['traceback']}", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Worker failed. See traceback above.")
+
+ for p in procs:
+ p.join()
+
+
+ # ------------------- 3. restore original order -------------------
+
+
+ restored_outputs = [None] * N
+ restored_steps = [None] * N
+
+ for item in seq_pairs:
+ if len(item) == 2:
+ gi, text = item
+ steps = None
+ else:
+ gi, text, steps = item
+ restored_outputs[gi] = text
+ restored_steps[gi] = steps
+
+
+ for i in range(N):
+ if restored_outputs[i] is None:
+ restored_outputs[i] = ""
+ if restored_steps[i] is None:
+ restored_steps[i] = ""
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_eval:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ else:
+ extracted_output = extract_code(full_output)
+ else:
+ extracted_output = extract_final_boxed_answer(full_output)
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ step_map_i = restored_steps[i] if restored_steps[i] is not None else []
+ #print(step_map_i)
+ data[index_i]["step_map"].append(step_map_i)
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+
+
diff --git a/sample/trace_viewer.html b/sample/trace_viewer.html
new file mode 100644
index 0000000000000000000000000000000000000000..841a65029ae692c86153dec3047036e9375beebf
--- /dev/null
+++ b/sample/trace_viewer.html
@@ -0,0 +1,154 @@
+
+
+
+
+Trace Viewer
+
+
+
+
+ Unmask Viewer
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/trado_rl_rollout.py b/sample/trado_rl_rollout.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d2ad4bd079d2c231f706994a4a7c63df8b6c181
--- /dev/null
+++ b/sample/trado_rl_rollout.py
@@ -0,0 +1,676 @@
+import os as _os
+_os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
+
+
+
+
+# Consolidate all caches into the local high-speed disk (NVMe or /dev/shm)
+# Local high-speed cache (NVMe or /dev/shm)
+_cache_root = "/dev/shm/torch_cache" # or "/local_nvme/torch_cache"
+_os.makedirs(_cache_root, exist_ok=True)
+_os.environ["TORCH_EXTENSIONS_DIR"] = _os.path.join(_cache_root, "torch_extensions")
+_os.environ["TRITON_CACHE_DIR"] = _os.path.join(_cache_root, "triton")
+_os.environ["XDG_CACHE_HOME"] = _cache_root
+_os.environ.setdefault("CUDA_MODULE_LOADING", "LAZY")
+
+
+
+_os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+_os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+_os.environ.pop("NCCL_BLOCKING_WAIT", None)
+_os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+
+import os
+import re
+import json
+from termcolor import cprint
+import random
+import torch.multiprocessing as mp
+from jinja2 import Template
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+
+
+import socket
+
+def _patch_safe_destroy():
+ import torch.distributed as dist
+ _real_destroy = dist.destroy_process_group
+ def _safe_destroy(group=None):
+ try:
+ if not dist.is_available():
+ return
+ try:
+ if not dist.is_initialized():
+ return
+ except Exception:
+ return
+ _real_destroy(group)
+ except AssertionError:
+ pass
+ dist.destroy_process_group = _safe_destroy
+
+
+
+def _llm_worker_run(args):
+ (model_path, tp, block_size, sampling_kwargs, vis_ids,
+ prompts_slice, indices_slice, enforce_eager, max_active, store_port) = args
+
+ import os
+ # 1) Setup environment (critical for correct worker behavior)
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.pop("NCCL_BLOCKING_WAIT", None)
+ os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, vis_ids))
+ #os.environ.setdefault("TORCH_EXTENSIONS_DIR", f"/tmp/torch_ext_worker_{store_port}")
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(store_port)
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
+
+ # 1.1) Create a per-worker `sitecustomize.py` and inject it into PYTHONPATH
+ # (this must be done before importing torch/jetengine)
+ patch_dir = f"/tmp/je_site_{store_port}"
+ os.makedirs(patch_dir, exist_ok=True)
+ patch_file = os.path.join(patch_dir, "sitecustomize.py")
+ # Important: the content must start at column 0 (no indentation)!
+ with open(patch_file, "w") as _f:
+ _f.write(
+ "import os\n"
+ "import torch.distributed as dist\n"
+ "_real = dist.init_process_group\n"
+ "def _wrapped(backend, init_method=None, *args, **kwargs):\n"
+ " port = os.environ.get('JE_TCP_PORT')\n"
+ " if port and isinstance(init_method, str) and init_method.startswith('tcp://localhost:2333'):\n"
+ " init_method = f'tcp://127.0.0.1:{port}'\n"
+ " return _real(backend, init_method, *args, **kwargs)\n"
+ "dist.init_process_group = _wrapped\n"
+ )
+ os.environ["PYTHONPATH"] = patch_dir + (":" + os.environ["PYTHONPATH"] if "PYTHONPATH" in os.environ else "")
+ os.environ["JE_TCP_PORT"] = str(store_port)
+
+ # 2) Import torch and patch the current worker process
+ import torch
+ import torch.distributed as dist
+ _patch_dist_port(store_port) # Patch port binding for this process
+ _patch_safe_destroy() # Avoid AssertionError in destroy_process_group
+ torch.cuda.set_device(0) # From this worker’s perspective, cuda:0 is the first visible device
+
+ # For debugging: print the worker’s CUDA_VISIBLE_DEVICES and assigned port
+ print(f"[worker pid={os.getpid()}] CVD={os.environ['CUDA_VISIBLE_DEVICES']}, port={store_port}, prompts={len(prompts_slice)}", flush=True)
+
+ # 3) Import jetengine and create the engine
+ # (child processes inherit the sitecustomize patch)
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=tp,
+ mask_token_id=151669,
+ block_length=block_size
+ )
+ sp = SamplingParams(**sampling_kwargs)
+ outs = llm.generate_streaming(prompts_slice, sp, max_active=max_active)
+ #seq = [o["text"] for o in outs]
+ #print(outs[0]["first_unmask_times"])
+ triples = []
+ for j, o in enumerate(outs):
+ triples.append((
+ indices_slice[j], # Global index (used to restore original order)
+ o["text"], # Generated text
+ o.get("first_unmask_times", None) # Optional time series aligned with completion tokens
+ ))
+
+ try:
+ if hasattr(llm, "shutdown"):
+ llm.shutdown()
+ except Exception:
+ pass
+
+ return triples
+
+
+
+def _llm_worker_entry(args, out_q):
+ import traceback, os
+ try:
+ res = _llm_worker_run(args)
+ out_q.put(("ok", res))
+ except Exception:
+ out_q.put(("err", {
+ "pid": os.getpid(),
+ "port": args[-1], # store_port
+ "traceback": traceback.format_exc(),
+ }))
+
+
+def _find_free_port():
+ s = socket.socket(); s.bind(('', 0))
+ p = s.getsockname()[1]; s.close()
+ return p
+
+def _patch_dist_port(port: int):
+ import torch.distributed as _dist
+ _real_init = _dist.init_process_group
+
+ def _wrapped(backend, init_method=None, *args, **kwargs):
+ # jetengine internally hardcodes "tcp://localhost:2333" — replace the port here
+ if isinstance(init_method, str) and init_method.startswith("tcp://localhost:2333"):
+ init_method = f"tcp://127.0.0.1:{port}"
+ return _real_init(backend, init_method, *args, **kwargs)
+
+ _dist.init_process_group = _wrapped
+
+
+import random
+def random_select(data_list, random_k):
+ data_list = random.sample(data_list, random_k)
+ return data_list
+
+if __name__ == "__main__":
+
+
+
+ tp = int(get_config().rollout.tensor_parallel_size) # Or check after loading config
+
+ if tp == 1:
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ # These two are NCCL’s own variables, keep using the NCCL_ prefix
+ os.environ.setdefault("NCCL_P2P_DISABLE", "1")
+ os.environ.setdefault("NCCL_IB_DISABLE", "1")
+ else:
+ # For multi-GPU communication, do not disable P2P/IB;
+ # also clean up related variables (both old and new names)
+ for k in [
+ "NCCL_P2P_DISABLE", "NCCL_IB_DISABLE",
+ "TORCH_NCCL_BLOCKING_WAIT", "TORCH_NCCL_ASYNC_ERROR_HANDLING",
+ "NCCL_BLOCKING_WAIT", "NCCL_ASYNC_ERROR_HANDLING",
+ ]:
+ os.environ.pop(k, None)
+
+
+
+ from transformers import AutoTokenizer
+
+ # --- graceful shutdown & unique port ---
+ import os, sys, atexit, signal, torch.distributed as dist
+
+
+ # 2) Automatically set compile architecture according to the local GPU
+ # (do NOT hardcode 8.0)
+ def _set_arch():
+ try:
+ if torch.cuda.is_available():
+ major, minor = torch.cuda.get_device_capability(0)
+ os.environ["TORCH_CUDA_ARCH_LIST"] = f"{major}.{minor}"
+ except Exception:
+ pass
+ _set_arch()
+
+
+
+ # 1) Use a new port at each startup to avoid conflicts with 2333
+ if "MASTER_PORT" not in os.environ:
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(_find_free_port())
+ # (If JetEngine hardcodes tcp://localhost:2333 instead of using env://,
+ # see the “special case” section at the end)
+
+ # 2) Intercept Ctrl-C/TERM to destroy distributed groups & engine gracefully
+ _llm = None
+ _child_ps = [] # If you create your own mp.Process/Pool, append objects here
+
+ def _cleanup():
+ # 2.1) Shutdown JetEngine engine (if API available)
+ global _llm
+ try:
+ if _llm is not None and hasattr(_llm, "shutdown"):
+ _llm.shutdown()
+ except Exception:
+ pass
+ # 2.3) Kill/join child processes
+ for p in _child_ps:
+ try:
+ if hasattr(p, "terminate"): p.terminate()
+ except Exception:
+ pass
+ for p in _child_ps:
+ try:
+ if hasattr(p, "join"): p.join(timeout=2)
+ except Exception:
+ pass
+
+ atexit.register(_cleanup)
+ def _sig_handler(sig, frame):
+ _cleanup()
+ # 130: standard exit code for SIGINT; 143: for SIGTERM
+ sys.exit(130 if sig == signal.SIGINT else 143)
+
+ signal.signal(signal.SIGINT, _sig_handler)
+ signal.signal(signal.SIGTERM, _sig_handler)
+
+
+
+
+
+
+
+
+
+ config = get_config()
+
+ try:
+ if mp.get_start_method(allow_none=True) != "spawn":
+ mp.set_start_method("spawn", force=True)
+ except RuntimeError:
+ pass
+
+
+
+ system_prompts = '''<|im_start|>user\n{{problem}}\nPlease reason step by step, and put your final answer within \\boxed{}.<|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts = '''<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+
+
+ project_name = config.experiment.project
+
+ if config.experiment.current_epoch == 1:
+ pretrained_model = config.model.pretrained_model
+ else:
+ pretrained_model = "../" + project_name + "/ckpt/" + config.model.optimized_name
+
+ code_task = False
+ if config.experiment.function == "train":
+ dataset = config.dataset.train_dataset
+ k_sample = config.rollout.num_response_per_task
+
+ if config.dataset.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+
+ outputs_name = "rl-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+ elif config.experiment.function == "evaluation":
+ dataset = config.evaluation.eval_dataset
+ if config.evaluation.data_type == "code":
+ code_task = True
+ system_prompts_function = '''<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+
+ k_sample = config.evaluation.num_response_per_task
+
+ config.rollout.tensor_parallel_size = config.evaluation.tensor_parallel_size
+ config.rollout.max_active = config.evaluation.max_active
+ config.rollout.max_token = config.evaluation.max_token
+ config.rollout.remasking_strategy = config.evaluation.remasking_strategy
+ config.rollout.dynamic_threshold = config.evaluation.dynamic_threshold
+ config.rollout.denoising_steps_per_block = config.evaluation.denoising_steps_per_block
+ config.rollout.temperature = config.evaluation.temperature
+ config.rollout.top_p = config.evaluation.top_p
+ config.rollout.top_k = config.evaluation.top_k
+ config.rollout.block_size = config.evaluation.block_size
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+
+
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(8)]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ if config.experiment.function == "train":
+ random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+
+ if config.experiment.function == "train":
+ random_select_num = config.rollout.num_task_per_step
+ random_select_num = int(random_select_num / num_node)
+ random_select_num = min(random_select_num, len(data))
+ data = random_select(data, random_select_num)
+ num = len(data)
+
+ model_path = os.path.expanduser(pretrained_model)
+ tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
+ # Initialize the LLM
+
+ block_size = config.rollout.block_size
+
+
+
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_task:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+
+ import torch, math
+ print(f"[preflight] CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES')}")
+ print(f"[preflight] parent sees torch.cuda.device_count()={torch.cuda.device_count()}")
+
+ cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
+ if cvd:
+ visible_gpus = [x.strip() for x in cvd.split(",") if x.strip() != ""]
+ device_ids = [int(x) for x in visible_gpus]
+ else:
+ device_ids = list(range(torch.cuda.device_count()))
+
+ gpu_num = len(device_ids)
+ tp = int(config.rollout.tensor_parallel_size)
+ assert gpu_num >= tp, f"Visible GPUs ({gpu_num}) < tensor_parallel_size ({tp})."
+ assert gpu_num >= 1, "No GPU visible"
+ if tp > 1:
+ ngroups = 1
+ else:
+ ngroups = max(1, gpu_num // max(1, tp))
+
+ groups = [ device_ids[i*tp : (i+1)*tp] for i in range(ngroups) ]
+
+ sampling_kwargs = dict(
+ temperature = config.rollout.temperature,
+ topk = config.rollout.top_k,
+ topp = config.rollout.top_p,
+ max_tokens = config.rollout.max_token,
+ remasking_strategy = config.rollout.remasking_strategy,
+ block_length = block_size,
+ denoising_steps = config.rollout.denoising_steps_per_block,
+ dynamic_threshold = config.rollout.dynamic_threshold
+ )
+ max_active_local = config.rollout.max_active
+
+ def _chunk_by_groups(lst, ng):
+ L = len(lst)
+ if ng <= 1: return [lst]
+ chunk_size = math.ceil(L / ng)
+ return [ lst[i*chunk_size : min((i+1)*chunk_size, L)] for i in range(ng) ]
+
+ prompt_chunks = _chunk_by_groups(shuffled_prompts, ngroups)
+ index_chunks = _chunk_by_groups(shuffled_idx, ngroups)
+
+ for a, b in zip(prompt_chunks, index_chunks):
+ assert len(a) == len(b)
+
+ seq_pairs = []
+
+ if ngroups == 1:
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, groups[0]))
+ import torch
+ torch.cuda.set_device(0)
+
+ if config.rollout.tensor_parallel_size > 1:
+ enforce_eager = False
+ else:
+ enforce_eager = True
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=config.rollout.tensor_parallel_size,
+ mask_token_id=151669, # Optional: only needed for masked/diffusion models
+ block_length=block_size
+ )
+ _llm = llm
+
+ # Set sampling/generation parameters
+ sampling_params = SamplingParams(
+ temperature=config.rollout.temperature,
+ topk=config.rollout.top_k,
+ topp=config.rollout.top_p,
+ max_tokens=config.rollout.max_token,
+ remasking_strategy=config.rollout.remasking_strategy,
+ block_length=block_size,
+ denoising_steps=config.rollout.denoising_steps_per_block,
+ dynamic_threshold=config.rollout.dynamic_threshold
+ )
+ try:
+ outputs = llm.generate_streaming(prompt_chunks[0], sampling_params, max_active=config.rollout.max_active)
+ for j, o in enumerate(outputs):
+ seq_pairs.append( (
+ index_chunks[0][j],
+ o["text"],
+ o.get("first_unmask_times", None)
+ ) )
+ finally:
+ _cleanup()
+ else:
+ ctx = mp.get_context("spawn")
+ enforce_eager_local = False if tp > 1 else True
+
+ base_port = 29000
+ store_ports = [base_port + g for g in range(ngroups)]
+
+ out_q = ctx.Queue()
+ procs = []
+ for g in range(ngroups):
+ if len(prompt_chunks[g]) == 0:
+ continue
+ args = (
+ model_path, tp, block_size, sampling_kwargs, groups[g],
+ prompt_chunks[g], index_chunks[g],
+ enforce_eager_local, max_active_local, store_ports[g],
+ )
+ p = ctx.Process(target=_llm_worker_entry, args=(args, out_q), daemon=False)
+ p.start()
+ procs.append(p)
+ _child_ps.append(p)
+
+ import queue, time
+
+ results_needed = len(procs)
+ results_got = 0
+
+ while results_got < results_needed:
+ try:
+ kind, payload = out_q.get(timeout=1800)
+ except queue.Empty:
+ dead = [p for p in procs if not p.is_alive()]
+ if dead:
+ for p in dead:
+ print(f"[parent] worker pid={p.pid} exitcode={p.exitcode} (no result)", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Some workers died without returning results. See logs above.")
+ continue
+
+ if kind == "ok":
+ seq_pairs.extend(payload)
+ results_got += 1
+ else: # "err"
+ print(f"[parent] worker error on port {payload['port']} pid {payload['pid']}:\n{payload['traceback']}", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Worker failed. See traceback above.")
+
+ for p in procs:
+ p.join()
+
+
+ # ------------------- 3. restore original order -------------------
+
+
+ restored_outputs = [None] * N
+ restored_steps = [None] * N
+
+ for item in seq_pairs:
+ if len(item) == 2:
+ gi, text = item
+ steps = None
+ else:
+ gi, text, steps = item
+ restored_outputs[gi] = text
+ restored_steps[gi] = steps
+
+
+ for i in range(N):
+ if restored_outputs[i] is None:
+ restored_outputs[i] = ""
+ if restored_steps[i] is None:
+ restored_steps[i] = ""
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_task:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ else:
+ extracted_output = extract_code(full_output)
+ else:
+ extracted_output = extract_final_boxed_answer(full_output)
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ step_map_i = restored_steps[i] if restored_steps[i] is not None else []
+ #print(step_map_i)
+ data[index_i]["step_map"].append(step_map_i)
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+
+
diff --git a/sample/trado_sample.py b/sample/trado_sample.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e6ee6115ab5542d071c2d28eed8ecd74edb0281
--- /dev/null
+++ b/sample/trado_sample.py
@@ -0,0 +1,693 @@
+import os as _os
+_os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
+
+
+
+# Consolidate all caches into the local high-speed disk (NVMe or /dev/shm)
+# Local high-speed cache (NVMe or /dev/shm)
+_cache_root = "/dev/shm/torch_cache" # or "/local_nvme/torch_cache"
+_os.makedirs(_cache_root, exist_ok=True)
+_os.environ["TORCH_EXTENSIONS_DIR"] = _os.path.join(_cache_root, "torch_extensions")
+_os.environ["TRITON_CACHE_DIR"] = _os.path.join(_cache_root, "triton")
+_os.environ["XDG_CACHE_HOME"] = _cache_root
+_os.environ.setdefault("CUDA_MODULE_LOADING", "LAZY")
+
+
+_os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+_os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+_os.environ.pop("NCCL_BLOCKING_WAIT", None)
+_os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+
+import os
+import re
+import json
+from termcolor import cprint
+import random
+import torch.multiprocessing as mp
+from jinja2 import Template
+
+from omegaconf import DictConfig, ListConfig, OmegaConf
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ conf = OmegaConf.merge(yaml_conf, cli_conf)
+ return conf
+
+
+
+# obtain prompt
+def get_prompt(data_i):
+ return Template(system_prompts).render(problem = data_i["question"])
+
+def extract_final_boxed_answer(s: str):
+ tag = r'\boxed{'
+ start = s.rfind(tag) # last \boxed{
+ if start == -1:
+ return "Can not extract the answer!"
+
+ i = start + len(tag)
+ depth = 1 # we are already inside one '{'
+ buf = []
+
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0: # matching '}' for the opening \boxed{
+ break
+ buf.append(ch)
+ i += 1
+
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+
+
+def extract_code(full_output):
+ matches = re.findall(r"```python(.*?)```", full_output, re.DOTALL)
+ if matches:
+ code_output = matches[-1].strip()
+ else:
+ code_output = "We can not extract the code in the output. "
+ return code_output
+
+
+'''
+def get_data_chunk(data, num_node, node_idx):
+ total = len(data)
+ chunk_size = (total + num_node - 1) // num_node
+ start_idx = node_idx * chunk_size
+ end_idx = min((node_idx + 1) * chunk_size, total)
+ return data[start_idx:end_idx]
+'''
+def get_data_chunk(data, num_nodes, node_idx):
+ total = len(data)
+ start = (total * node_idx) // num_nodes
+ end = (total * (node_idx + 1)) // num_nodes
+ return data[start:end]
+
+
+import socket
+
+def _patch_safe_destroy():
+ import torch.distributed as dist
+ _real_destroy = dist.destroy_process_group
+ def _safe_destroy(group=None):
+ try:
+ if not dist.is_available():
+ return
+ try:
+ if not dist.is_initialized():
+ return
+ except Exception:
+ return
+ _real_destroy(group)
+ except AssertionError:
+ pass
+ dist.destroy_process_group = _safe_destroy
+
+
+
+def _llm_worker_run(args):
+ (model_path, tp, block_size, sampling_kwargs, vis_ids,
+ prompts_slice, indices_slice, enforce_eager, max_active, store_port) = args
+
+ import os
+ # 1) Setup environment (critical for correct worker behavior)
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.pop("NCCL_BLOCKING_WAIT", None)
+ os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, vis_ids))
+ #os.environ.setdefault("TORCH_EXTENSIONS_DIR", f"/tmp/torch_ext_worker_{store_port}")
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(store_port)
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
+
+ # 1.1) Create a per-worker `sitecustomize.py` and inject it into PYTHONPATH
+ # (this must be done before importing torch/jetengine)
+ patch_dir = f"/tmp/je_site_{store_port}"
+ os.makedirs(patch_dir, exist_ok=True)
+ patch_file = os.path.join(patch_dir, "sitecustomize.py")
+ # Important: the content must start at column 0 (no indentation)!
+ with open(patch_file, "w") as _f:
+ _f.write(
+ "import os\n"
+ "import torch.distributed as dist\n"
+ "_real = dist.init_process_group\n"
+ "def _wrapped(backend, init_method=None, *args, **kwargs):\n"
+ " port = os.environ.get('JE_TCP_PORT')\n"
+ " if port and isinstance(init_method, str) and init_method.startswith('tcp://localhost:2333'):\n"
+ " init_method = f'tcp://127.0.0.1:{port}'\n"
+ " return _real(backend, init_method, *args, **kwargs)\n"
+ "dist.init_process_group = _wrapped\n"
+ )
+ os.environ["PYTHONPATH"] = patch_dir + (":" + os.environ["PYTHONPATH"] if "PYTHONPATH" in os.environ else "")
+ os.environ["JE_TCP_PORT"] = str(store_port)
+
+ # 2) Import torch and patch the current worker process
+ import torch
+ import torch.distributed as dist
+ _patch_dist_port(store_port) # Patch port binding for this process
+ _patch_safe_destroy() # Avoid AssertionError in destroy_process_group
+ torch.cuda.set_device(0) # From this worker’s perspective, cuda:0 is the first visible device
+
+ # For debugging: print the worker’s CUDA_VISIBLE_DEVICES and assigned port
+ print(f"[worker pid={os.getpid()}] CVD={os.environ['CUDA_VISIBLE_DEVICES']}, port={store_port}, prompts={len(prompts_slice)}", flush=True)
+
+ # 3) Import jetengine and create the engine
+ # (child processes inherit the sitecustomize patch)
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ llm = None
+ triples = []
+ try:
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=tp,
+ mask_token_id=151669,
+ block_length=block_size
+ )
+ sp = SamplingParams(**sampling_kwargs)
+
+ # Keep max_active sane for each worker’s slice to avoid rare internal exits
+ local_max_active = min(max_active, max(1, len(prompts_slice)))
+ outs = llm.generate_streaming(prompts_slice, sp, max_active=local_max_active)
+
+ # Collect results incrementally so we can return partials on any exit
+ for j, o in enumerate(outs):
+ triples.append((
+ indices_slice[j],
+ o["text"],
+ o.get("first_unmask_times", None)
+ ))
+ except BaseException as e:
+ # Swallow SystemExit/KeyboardInterrupt/etc. so we can return partials
+ print(f"[worker pid={os.getpid()}] Caught {type(e).__name__}: {e}. Returning partial results ({len(triples)})", flush=True)
+ finally:
+ try:
+ if llm is not None and hasattr(llm, "shutdown"):
+ llm.shutdown()
+ except Exception:
+ pass
+
+ return triples
+
+
+
+def _llm_worker_entry(args, out_q):
+ import traceback, os
+ try:
+ res = _llm_worker_run(args)
+ # Even if partial, report as 'ok' so parent can use what we have
+ out_q.put(("ok", res))
+ except BaseException:
+ tb = traceback.format_exc()
+ # Fall back to 'err' path if even the call above exploded
+ try:
+ out_q.put(("err", {
+ "pid": os.getpid(),
+ "port": args[-1],
+ "traceback": tb,
+ }))
+ except Exception:
+ pass
+
+
+def _find_free_port():
+ s = socket.socket(); s.bind(('', 0))
+ p = s.getsockname()[1]; s.close()
+ return p
+
+def _patch_dist_port(port: int):
+ import torch.distributed as _dist
+ _real_init = _dist.init_process_group
+
+ def _wrapped(backend, init_method=None, *args, **kwargs):
+ # jetengine internally hardcodes "tcp://localhost:2333" — replace the port here
+ if isinstance(init_method, str) and init_method.startswith("tcp://localhost:2333"):
+ init_method = f"tcp://127.0.0.1:{port}"
+ return _real_init(backend, init_method, *args, **kwargs)
+
+ _dist.init_process_group = _wrapped
+
+
+
+if __name__ == "__main__":
+
+ config = get_config()
+
+
+
+ tp = int(get_config().rollout.tensor_parallel_size) # Or check after loading config
+
+ if tp == 1:
+ os.environ.setdefault("TORCH_NCCL_ASYNC_ERROR_HANDLING", "1")
+ os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1")
+ # These two are NCCL’s own variables, keep using the NCCL_ prefix
+ os.environ.setdefault("NCCL_P2P_DISABLE", "1")
+ os.environ.setdefault("NCCL_IB_DISABLE", "1")
+ else:
+ # For multi-GPU communication, do not disable P2P/IB;
+ # also clean up related variables (both old and new names)
+ for k in [
+ "NCCL_P2P_DISABLE", "NCCL_IB_DISABLE",
+ "TORCH_NCCL_BLOCKING_WAIT", "TORCH_NCCL_ASYNC_ERROR_HANDLING",
+ "NCCL_BLOCKING_WAIT", "NCCL_ASYNC_ERROR_HANDLING",
+ ]:
+ os.environ.pop(k, None)
+
+
+
+ from transformers import AutoTokenizer
+
+ # --- graceful shutdown & unique port ---
+ import os, sys, atexit, signal, torch.distributed as dist
+
+ # 2) Automatically set compile architecture according to the local GPU
+ # (do NOT hardcode 8.0)
+ def _set_arch():
+ try:
+ if torch.cuda.is_available():
+ major, minor = torch.cuda.get_device_capability(0)
+ os.environ["TORCH_CUDA_ARCH_LIST"] = f"{major}.{minor}"
+ except Exception:
+ pass
+ _set_arch()
+
+
+
+ # 1) Use a new port at each startup to avoid conflicts with 2333
+ if "MASTER_PORT" not in os.environ:
+ os.environ["MASTER_ADDR"] = "127.0.0.1"
+ os.environ["MASTER_PORT"] = str(_find_free_port())
+ # (If JetEngine hardcodes tcp://localhost:2333 instead of using env://,
+ # see the “special case” section at the end)
+
+ # 2) Intercept Ctrl-C/TERM to destroy distributed groups & engine gracefully
+ _llm = None
+ _child_ps = [] # If you create your own mp.Process/Pool, append objects here
+
+ def _cleanup():
+ # 2.1) Shutdown JetEngine engine (if API available)
+ global _llm
+ try:
+ if _llm is not None and hasattr(_llm, "shutdown"):
+ _llm.shutdown()
+ except Exception:
+ pass
+ # 2.3) Kill/join child processes
+ for p in _child_ps:
+ try:
+ if hasattr(p, "terminate"): p.terminate()
+ except Exception:
+ pass
+ for p in _child_ps:
+ try:
+ if hasattr(p, "join"): p.join(timeout=2)
+ except Exception:
+ pass
+
+ atexit.register(_cleanup)
+ def _sig_handler(sig, frame):
+ _cleanup()
+ # 130: standard exit code for SIGINT; 143: for SIGTERM
+ sys.exit(130 if sig == signal.SIGINT else 143)
+
+ signal.signal(signal.SIGINT, _sig_handler)
+ signal.signal(signal.SIGTERM, _sig_handler)
+
+
+
+
+
+
+
+
+ try:
+ if mp.get_start_method(allow_none=True) != "spawn":
+ mp.set_start_method("spawn", force=True)
+ except RuntimeError:
+ pass
+
+
+ k_sample = config.rollout.num_response_per_task
+ # non-cot prompt
+ #system_prompts = '''<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+ # cot prompt
+ system_prompts = '''<|im_start|>user\n{{problem}}\nPlease reason step by step, and put your final answer within \\boxed{}.<|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts = '''<|im_start|>user\nYou need to put your final answer in \\boxed{}. This is the problem:\n{{problem}}<|im_end|>\n<|im_start|>assistant\n'''
+
+ project_name = config.experiment.project
+
+ code_eval = False
+
+ dataset = config.dataset.eval_dataset
+ pretrained_model = config.model
+ if config.dataset.data_type == "code":
+ code_eval = True
+ system_prompts_function = '''<|im_start|>user\n{{problem}}\nPlace your code within a single Python code block ```python ```. Do not include more than one code block. <|im_end|>\n<|im_start|>assistant\n'''
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts_stdio = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou should put your code in ```python ```. Use input() to read input and print() to produce output in your script. <|im_end|>\n<|im_start|>assistant\n'''
+ elif config.dataset.data_type == "option":
+ system_prompts = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou need to think step by step and put the final option (A, B, C, or D only—no other character) in \\boxed{}. <|im_end|>\n<|im_start|>assistant\n'''
+ if config.rollout.start_with_think:
+ system_prompts = '''<|im_start|>user\nThis is the problem:\n{{problem}}\nYou need to think step by step and put the final option (A, B, C, or D only—no other character) in \\boxed{}. <|im_end|>\n<|im_start|>assistant\n'''
+
+ outputs_name = "eval-" + pretrained_model.replace("/", ".") + "-" + dataset
+
+ with open("../data/" + dataset + ".json", 'r') as f:
+ data = json.load(f)
+ #data = [data[i] for i in range(100, 300)]
+ #data = data[400:]
+
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ if num_node > 1:
+ #random.shuffle(data)
+ data = get_data_chunk(data, num_node, node_index)
+
+ num = len(data)
+
+
+ model_path = os.path.expanduser(pretrained_model)
+ tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
+ # Initialize the LLM
+
+ block_size = config.rollout.block_size
+
+
+
+
+
+
+
+
+
+
+
+ # initialization
+ generation_prompts = []
+ prefix_list = []
+ index_list = []
+ for i in range(num):
+ # preprocess
+ if code_eval:
+ if data[i]["test_method"] == "stdio":
+ system_prompts = system_prompts_stdio
+ prefix_list = prefix_list + [None] * k_sample
+ else:
+ system_prompts = system_prompts_function + data[i]["prefix"]
+ prefix_list = prefix_list + [data[i]["prefix"]] * k_sample
+ generation_prompts = generation_prompts + [get_prompt(data[i])] * k_sample
+
+ index_list = index_list + [i] * k_sample
+ data[i]["full_output"] = []
+ data[i]["step_map"] = []
+ data[i]["extracted_output"] = []
+ data[i]["response_length"] = []
+ data[i]["prompt"] = get_prompt(data[i])
+
+
+
+
+
+ # --------------------------- 1. shuffle --------------------------
+ cprint("start generation...", "green")
+
+ all_prompts = generation_prompts
+ N = len(all_prompts)
+
+ shuffled_idx = list(range(N))
+ random.shuffle(shuffled_idx)
+ shuffled_prompts = [all_prompts[i] for i in shuffled_idx]
+
+
+ import torch, math
+ print(f"[preflight] CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES')}")
+ print(f"[preflight] parent sees torch.cuda.device_count()={torch.cuda.device_count()}")
+
+ cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
+ if cvd:
+ visible_gpus = [x.strip() for x in cvd.split(",") if x.strip() != ""]
+ device_ids = [int(x) for x in visible_gpus]
+ else:
+ device_ids = list(range(torch.cuda.device_count()))
+
+ gpu_num = len(device_ids)
+ tp = int(config.rollout.tensor_parallel_size)
+ assert gpu_num >= tp, f"Visible GPUs ({gpu_num}) < tensor_parallel_size ({tp})."
+ assert gpu_num >= 1, "No GPU visible"
+ if tp > 1:
+ ngroups = 1
+ else:
+ ngroups = max(1, gpu_num // max(1, tp))
+
+ groups = [ device_ids[i*tp : (i+1)*tp] for i in range(ngroups) ]
+
+
+
+ def to_single_token_stop_ids(tokenizer, stop_token_list):
+ if not stop_token_list:
+ return []
+ ids, seen = [], set()
+ for s in stop_token_list:
+ if isinstance(s, int):
+ tid = [s]
+ elif isinstance(s, str):
+ tid = tokenizer.encode(s, add_special_tokens=False)
+ elif isinstance(s, (list, tuple)) and all(isinstance(x, int) for x in s):
+ tid = list(s)
+ else:
+ continue
+ if len(tid) == 1:
+ t = tid[0]
+ if t not in seen:
+ seen.add(t)
+ ids.append(t)
+ return ids
+
+ from omegaconf import MISSING
+ if OmegaConf.select(config, "rollout.stop_token_list", default=MISSING) is not MISSING:
+ stop_token_id_list = to_single_token_stop_ids(tokenizer, config.rollout.stop_token_list)
+ else:
+ stop_token_id_list = []
+
+ sampling_kwargs = dict(
+ temperature = config.rollout.temperature,
+ topk = config.rollout.top_k,
+ topp = config.rollout.top_p,
+ max_tokens = config.rollout.max_token,
+ remasking_strategy = config.rollout.remasking_strategy,
+ block_length = block_size,
+ denoising_steps = config.rollout.denoising_steps_per_block,
+ dynamic_threshold = config.rollout.dynamic_threshold,
+ stop_words = stop_token_id_list
+ )
+ max_active_local = config.rollout.max_active
+
+ def _chunk_by_groups(lst, ng):
+ L = len(lst)
+ if ng <= 1: return [lst]
+ chunk_size = math.ceil(L / ng)
+ return [ lst[i*chunk_size : min((i+1)*chunk_size, L)] for i in range(ng) ]
+
+ prompt_chunks = _chunk_by_groups(shuffled_prompts, ngroups)
+ index_chunks = _chunk_by_groups(shuffled_idx, ngroups)
+
+ for a, b in zip(prompt_chunks, index_chunks):
+ assert len(a) == len(b)
+
+ seq_pairs = []
+
+ if ngroups == 1:
+ from jetengine_ext.llm import LLM
+ from jetengine_ext.sampling_params import SamplingParams
+
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, groups[0]))
+ import torch
+ torch.cuda.set_device(0)
+
+ if config.rollout.tensor_parallel_size > 1:
+ enforce_eager = False
+ else:
+ enforce_eager = True
+ llm = LLM(
+ model_path,
+ enforce_eager=enforce_eager,
+ tensor_parallel_size=config.rollout.tensor_parallel_size,
+ mask_token_id=151669, # Optional: only needed for masked/diffusion models
+ block_length=block_size
+ )
+ _llm = llm
+
+ # Set sampling/generation parameters
+ sampling_params = SamplingParams(
+ temperature=config.rollout.temperature,
+ topk=config.rollout.top_k,
+ topp=config.rollout.top_p,
+ max_tokens=config.rollout.max_token,
+ remasking_strategy=config.rollout.remasking_strategy,
+ block_length=block_size,
+ denoising_steps=config.rollout.denoising_steps_per_block,
+ dynamic_threshold=config.rollout.dynamic_threshold,
+ stop_words = stop_token_id_list
+ )
+ try:
+ outputs = llm.generate_streaming(prompt_chunks[0], sampling_params, max_active=config.rollout.max_active)
+ for j, o in enumerate(outputs):
+ seq_pairs.append( (
+ index_chunks[0][j],
+ o["text"],
+ o.get("first_unmask_times", None)
+ ) )
+ finally:
+ _cleanup()
+ else:
+ import time
+ ctx = mp.get_context("spawn")
+ enforce_eager_local = False if tp > 1 else True
+
+ base_port = 29000
+ store_ports = [base_port + g for g in range(ngroups)]
+
+ out_q = ctx.Queue()
+ procs = []
+ for g in range(ngroups):
+ if len(prompt_chunks[g]) == 0:
+ continue
+ args = (
+ model_path, tp, block_size, sampling_kwargs, groups[g],
+ prompt_chunks[g], index_chunks[g],
+ enforce_eager_local, max_active_local, store_ports[g],
+ )
+ p = ctx.Process(target=_llm_worker_entry, args=(args, out_q), daemon=False)
+ p.start()
+ #time.sleep(2)
+ procs.append(p)
+ _child_ps.append(p)
+
+ import queue, time
+
+ results_needed = len(procs)
+ results_got = 0
+
+ while results_got < results_needed:
+ try:
+ kind, payload = out_q.get(timeout=3600 * 24)
+ except queue.Empty:
+ dead = [p for p in procs if not p.is_alive()]
+ if dead:
+ for p in dead:
+ print(f"[parent] worker pid={p.pid} exitcode={p.exitcode} (no result)", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Some workers died without returning results. See logs above.")
+ continue
+
+ if kind == "ok":
+ seq_pairs.extend(payload)
+ results_got += 1
+ else: # "err"
+ print(f"[parent] worker error on port {payload['port']} pid {payload['pid']}:\n{payload['traceback']}", flush=True)
+ for p in procs:
+ if p.is_alive():
+ p.terminate()
+ for p in procs:
+ p.join(timeout=5)
+ raise RuntimeError("Worker failed. See traceback above.")
+
+ for p in procs:
+ p.join()
+
+
+ # ------------------- 3. restore original order -------------------
+
+
+ restored_outputs = [None] * N
+ restored_steps = [None] * N
+
+ for item in seq_pairs:
+ if len(item) == 2:
+ gi, text = item
+ steps = None
+ else:
+ gi, text, steps = item
+ restored_outputs[gi] = text
+ restored_steps[gi] = steps
+
+
+ for i in range(N):
+ if restored_outputs[i] is None:
+ restored_outputs[i] = ""
+ if restored_steps[i] is None:
+ restored_steps[i] = ""
+
+ cprint("generation job done!", "green")
+
+
+
+
+
+
+ def get_token_lengths(strings, tokenizer):
+ pad_token = tokenizer.pad_token
+
+ escaped = re.escape(pad_token)
+ pattern = rf"(?:{escaped})+"
+ remove_pattern = escaped
+
+ collapse_re = re.compile(pattern)
+
+ lengths = []
+ for s in strings:
+ s_clean = collapse_re.sub(lambda _: pad_token if isinstance(pad_token, str) else '', s)
+ s_clean = re.sub(remove_pattern, '', s_clean)
+ lengths.append(len(tokenizer.encode(s_clean, add_special_tokens=False)))
+ return lengths
+
+ response_length = get_token_lengths(restored_outputs, tokenizer)
+ mean_response_length = sum(response_length) / len(response_length)
+
+
+
+
+ # process generated codes
+ i = 0
+ for full_output in restored_outputs:
+ if code_eval:
+ if data[int(i/k_sample)]["test_method"] == "function":
+ extracted_output = extract_code(prefix_list[i] + full_output)
+ else:
+ extracted_output = extract_code(full_output)
+ else:
+ extracted_output = extract_final_boxed_answer(full_output)
+ index_i = index_list[i]
+ data[index_i]["full_output"].append(full_output)
+ step_map_i = restored_steps[i] if restored_steps[i] is not None else []
+ #print(step_map_i)
+ data[index_i]["step_map"].append(step_map_i)
+ data[index_i]["extracted_output"].append(extracted_output)
+ data[index_i]["response_length"].append(response_length[i])
+ i += 1
+
+ # output the data
+ if num_node > 1:
+ output_file_name = "../" + project_name + f"/temp_data/outputs-{node_index}-" + outputs_name + ".json"
+ else:
+ output_file_name = "../" + project_name + "/temp_data/outputs-" + outputs_name + ".json"
+ os.makedirs(os.path.dirname(output_file_name), exist_ok=True)
+ with open(output_file_name, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+
+
diff --git a/sample/vlm_process_reward.py b/sample/vlm_process_reward.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5b5e4cb1e738e1f6cade8751adbe27af03d7a1e
--- /dev/null
+++ b/sample/vlm_process_reward.py
@@ -0,0 +1,576 @@
+# -*- coding: utf-8 -*-
+# Path: vlm_process_reward.py
+
+import os
+import re
+import json
+import copy
+import random
+import traceback
+from typing import List, Dict, Any, Tuple
+
+import multiprocessing as mp
+from jinja2 import Template
+from termcolor import cprint
+
+import torch
+from PIL import Image
+from transformers import AutoTokenizer, AutoProcessor
+from vllm import LLM, SamplingParams
+from qwen_vl_utils import process_vision_info
+from omegaconf import OmegaConf
+
+os.environ["TOKENIZERS_PARALLELISM"] = "false"
+
+
+# ==================== boxed 提取 ====================
+
+def extract_final_boxed_answer(s: str) -> str:
+ tag = r'\boxed{'
+ start = s.rfind(tag)
+ if start == -1:
+ return "Can not extract the answer!"
+ i = start + len(tag)
+ depth = 1
+ buf = []
+ while i < len(s) and depth:
+ ch = s[i]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ if depth == 0:
+ break
+ buf.append(ch)
+ i += 1
+ return ''.join(buf) if depth == 0 else "Can not extract the answer!"
+
+
+def normalize_reward_from_response(resp: str) -> int:
+ boxed = extract_final_boxed_answer(resp or "")
+ if boxed == "Can not extract the answer!":
+ return 0
+ val = boxed.strip()
+ if not re.fullmatch(r'[+\-]?1', val):
+ return 0
+ try:
+ v = int(val)
+ except Exception:
+ return 0
+ return v if v in (1, -1) else 0
+
+
+
+def _resolve_local_images(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ msgs = copy.deepcopy(messages)
+ for m in msgs:
+ if "content" not in m:
+ continue
+ for chunk in m["content"]:
+ if isinstance(chunk, dict) and chunk.get("type") == "image":
+ img_val = chunk.get("image")
+ if isinstance(img_val, str) and os.path.exists(img_val):
+ try:
+ chunk["image"] = Image.open(img_val).convert("RGB")
+ except Exception:
+ pass
+ return msgs
+
+
+def prepare_inputs_for_vllm(messages: List[Dict[str, Any]],
+ processor: AutoProcessor) -> Dict[str, Any]:
+ messages = _resolve_local_images(messages)
+ text = processor.apply_chat_template(
+ messages,
+ tokenize=False,
+ add_generation_prompt=True,
+ )
+
+ patch_size = getattr(
+ getattr(processor, "image_processor", object()),
+ "patch_size",
+ 14,
+ )
+ try:
+ image_inputs, video_inputs, video_kwargs = process_vision_info(
+ messages,
+ image_patch_size=patch_size,
+ return_video_kwargs=True,
+ return_video_metadata=True,
+ )
+ except TypeError:
+ image_inputs, video_inputs, video_kwargs = process_vision_info(
+ messages,
+ image_patch_size=patch_size,
+ return_video_kwargs=True,
+ )
+
+ mm_data: Dict[str, Any] = {}
+ if image_inputs is not None:
+ mm_data["image"] = image_inputs
+ if video_inputs is not None:
+ mm_data["video"] = video_inputs
+
+ return {
+ "prompt": text,
+ "multi_modal_data": mm_data,
+ "mm_processor_kwargs": video_kwargs,
+ }
+
+
+def build_reward_messages(abs_image_path: str,
+ text_prompt: str) -> List[Dict[str, Any]]:
+ if abs_image_path:
+ return [
+ {
+ "role": "system",
+ "content": (
+ "You are a helpful assistant that scores the correctness of "
+ "response excerpts based on the provided image and context."
+ ),
+ },
+ {
+ "role": "user",
+ "content": [
+ {"type": "image", "image": abs_image_path},
+ {"type": "text", "text": text_prompt},
+ ],
+ },
+ ]
+ else:
+ return [
+ {
+ "role": "system",
+ "content": (
+ "You are a helpful assistant that scores the correctness of "
+ "response excerpts based on the provided context."
+ ),
+ },
+ {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": text_prompt},
+ ],
+ },
+ ]
+
+
+
+def worker_fn(pretrained_model: str,
+ gpu_ids: List[int],
+ task_queue: mp.Queue,
+ result_queue: mp.Queue,
+ dtype: str,
+ gpu_mem: float,
+ max_model_len: int,
+ temperature: float,
+ top_p: float,
+ max_tokens: int,
+ stop_words: List[str]):
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, gpu_ids))
+ os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
+
+ print(f"[Worker-{gpu_ids}] Loading model...", flush=True)
+
+ processor = AutoProcessor.from_pretrained(pretrained_model)
+ llm = LLM(
+ model=pretrained_model,
+ trust_remote_code=True,
+ dtype=dtype,
+ tensor_parallel_size=len(gpu_ids),
+ gpu_memory_utilization=gpu_mem,
+ max_model_len=max_model_len if max_model_len > 0 else None,
+ enforce_eager=False,
+ )
+ sampling = SamplingParams(
+ temperature=temperature,
+ top_p=top_p,
+ max_tokens=max_tokens,
+ stop=stop_words,
+ )
+
+ while True:
+ task = task_queue.get()
+ if task == "STOP":
+ print(f"[Worker-{gpu_ids}] Stopping...", flush=True)
+ break
+
+ try:
+ task_id, msg_chunk, R = task # msg_chunk: List[messages]
+ except Exception:
+ print(f"[Worker-{gpu_ids}] Invalid task: {task}", flush=True)
+ continue
+
+ if not msg_chunk:
+ result_queue.put((task_id, []))
+ continue
+
+ try:
+ batch_inputs = []
+ for messages in msg_chunk:
+ inp = prepare_inputs_for_vllm(messages, processor)
+ batch_inputs.extend([inp] * R)
+
+ outs = llm.generate(batch_inputs, sampling_params=sampling)
+ texts = [o.outputs[0].text if o.outputs else "" for o in outs]
+ result_queue.put((task_id, texts))
+ except Exception as e:
+ print(f"[Worker-{gpu_ids}] ERROR in generate: {repr(e)}", flush=True)
+ traceback.print_exc()
+ result_queue.put((task_id, []))
+
+
+def start_workers(pretrained_model: str,
+ gpu_groups: List[List[int]],
+ dtype: str,
+ gpu_mem: float,
+ max_model_len: int,
+ temperature: float,
+ top_p: float,
+ max_tokens: int,
+ stop_words: List[str]):
+ task_queues, result_queues, procs = [], [], []
+ for gpu_ids in gpu_groups:
+ tq = mp.Queue()
+ rq = mp.Queue()
+ p = mp.Process(
+ target=worker_fn,
+ args=(
+ pretrained_model,
+ gpu_ids,
+ tq,
+ rq,
+ dtype,
+ gpu_mem,
+ max_model_len,
+ temperature,
+ top_p,
+ max_tokens,
+ stop_words,
+ ),
+ )
+ p.start()
+ task_queues.append(tq)
+ result_queues.append(rq)
+ procs.append(p)
+ return task_queues, result_queues, procs
+
+
+def stop_workers(task_queues: List[mp.Queue], processes: List[mp.Process]):
+ for q in task_queues:
+ try:
+ q.put("STOP")
+ except Exception:
+ pass
+ for p in processes:
+ try:
+ p.join()
+ except Exception:
+ pass
+
+
+def split_even(items: List[Any], n: int) -> List[List[Any]]:
+ if n <= 0:
+ return [items]
+ k, m = divmod(len(items), n)
+ return [
+ items[i * k + min(i, m):(i + 1) * k + min(i + 1, m)]
+ for i in range(n)
+ ]
+
+
+def generate_results(messages_all: List[List[Dict[str, Any]]],
+ R: int,
+ gpu_groups: List[List[int]],
+ task_queues: List[mp.Queue],
+ result_queues: List[mp.Queue]) -> List[str]:
+
+ num_engines = len(gpu_groups)
+ chunks = split_even(messages_all, num_engines)
+
+ jobs = []
+ for i, (q, chunk) in enumerate(zip(task_queues, chunks)):
+ if not chunk:
+ continue
+ q.put((i, chunk, R))
+ jobs.append(i)
+
+ results_by_job: Dict[int, List[str]] = {}
+ remaining = set(jobs)
+
+ idle_loops = 0
+ while remaining:
+ got_any = False
+ for i, rq in enumerate(result_queues):
+ if i not in remaining:
+ continue
+ try:
+ task_id, result = rq.get(timeout=0.5)
+ except Exception:
+ continue
+ results_by_job[task_id] = result
+ remaining.remove(task_id)
+ got_any = True
+
+ if not got_any:
+ idle_loops += 1
+ if idle_loops % 10 == 0:
+ print(f"[Main] Waiting for workers... remaining jobs: {sorted(remaining)}",
+ flush=True)
+ if idle_loops > 600: # 600*0.5s = 300s
+ raise RuntimeError(
+ f"[Main] generate_results stuck, remaining jobs: {sorted(remaining)}"
+ )
+
+ flat: List[str] = []
+ for i, chunk in enumerate(chunks):
+ if not chunk:
+ continue
+ flat.extend(results_by_job.get(i, []))
+ return flat
+
+
+# ==================== 配置 ====================
+
+def get_config():
+ cli_conf = OmegaConf.from_cli()
+ yaml_conf = OmegaConf.load(cli_conf.config)
+ return OmegaConf.merge(yaml_conf, cli_conf)
+
+
+# ==================== 主流程 ====================
+
+if __name__ == "__main__":
+ mp.set_start_method("spawn", force=True)
+
+ config = get_config()
+
+ reward_llm = config.model.process_reward_model
+
+ reward_chunk_length = config.model.process_reward_chunk_size
+
+ gpu_groups = [[0, 1, 2, 3], [4, 5, 6, 7]]
+ k_sample = 3
+ max_model_len = 15000
+ max_generation_token = 10000
+ temp = 0.6
+ top_p = 0.95
+ stop_words = [
+ "",
+ "User:",
+ "Human:",
+ "Assistant:",
+ "<|im_end|>",
+ "<|endoftext|>",
+ ]
+ dtype = "bfloat16"
+ gpu_mem = 0.85
+
+ project_name = config.experiment.project
+ dataset = config.dataset.train_dataset
+ num_node = config.experiment.num_node
+ node_index = config.experiment.node_index
+ base_dir = config.system.base_dir
+
+ if config.experiment.current_epoch == 1:
+ policy_model = config.model.pretrained_model
+ else:
+ policy_model = f"../{project_name}/ckpt/{config.model.optimized_name}"
+
+ outputs_name = "rl-" + policy_model.replace("/", ".") + "-" + dataset + f"-step{config.experiment.current_epoch}"
+ if num_node > 1:
+ input_path = f"../{project_name}/temp_data/outputs-{node_index}-{outputs_name}.json"
+ else:
+ input_path = f"../{project_name}/temp_data/outputs-{outputs_name}.json"
+
+
+ print(f"[Main] Load {input_path}", flush=True)
+ with open(input_path, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ num = len(data)
+ print(f"[Main] #items = {num}", flush=True)
+
+ print(f"[Main] Launch {len(gpu_groups)} engines: {gpu_groups} | R={k_sample}",
+ flush=True)
+ task_queues, result_queues, processes = start_workers(
+ pretrained_model=reward_llm,
+ gpu_groups=gpu_groups,
+ dtype=dtype,
+ gpu_mem=gpu_mem,
+ max_model_len=max_model_len,
+ temperature=temp,
+ top_p=top_p,
+ max_tokens=max_generation_token,
+ stop_words=stop_words,
+ )
+
+ system_prompts = """For the question below (with the provided image), you will be given an entire response (we have already known it is {{if_correct}}) and an excerpt (a middle part) from it. Your task is to grade the excerpt for correctness using 1 (correct) or -1 (incorrect).
+
+Rules:
+- Judge ONLY the excerpt's correctness given the question, the image content, and the whole response (which is {{if_correct}}) as context.
+- The excerpt may be truncated at the beginning or end due to chunking; do NOT penalize boundary truncation.
+- If the excerpt is consistent and logically justified, score 1.
+- If the excerpt contains logical error, score -1.
+- Ignore minor grammar issues that do not affect correctness.
+- You need to think step by step first then put your final score in \\boxed{}.
+
+This is the question:
+{{question}}
+
+This is the final ground truth answer:
+{{gt_answer}}
+
+This is the given response ({{if_correct}}):
+{{whole_solution}}
+
+This is the excerpt I need you to score:
+{{excerpt}}
+
+You need to think step by step first then put your final score in \\boxed{}.
+"""
+
+ def get_prompt_text(excerpt, question, correctness, full_output, gt_answer):
+ correctness_string = "correct" if correctness else "incorrect"
+ return Template(system_prompts).render(
+ excerpt=excerpt,
+ question=question,
+ if_correct=correctness_string,
+ whole_solution=full_output,
+ gt_answer=gt_answer,
+ )
+
+ # tokenizer for chunking
+ policy_tokenizer = AutoTokenizer.from_pretrained(
+ policy_model,
+ trust_remote_code=True,
+ )
+
+ messages_all: List[List[Dict[str, Any]]] = []
+ index_list: List[Tuple[int, int, int]] = []
+
+ def get_prefix_text_by_chunk_end(chunk_end: int,
+ token_ids: List[int]) -> str:
+ chunk_end = max(0, min(chunk_end, len(token_ids)))
+ if chunk_end <= 0:
+ return ""
+ return policy_tokenizer.decode(
+ token_ids[:chunk_end],
+ skip_special_tokens=True,
+ clean_up_tokenization_spaces=False,
+ )
+
+ print("[Main] Build prompts...", flush=True)
+ for i in range(num):
+ q_i = data[i]["question"]
+ n_j = len(data[i]["full_output"])
+
+ data[i]["process_reward_list"] = []
+ data[i]["reward_prompt"] = [[] for _ in range(n_j)]
+ data[i]["process_reward_response"] = []
+
+ rel_image = data[i].get("image", "")
+ abs_image = os.path.join(config.dataset.image_root, rel_image) if rel_image else ""
+
+ for j in range(n_j):
+ data[i]["process_reward_list"].append([])
+ data[i]["process_reward_response"].append([])
+
+ full_j = data[i]["full_output"][j]
+ length_j = data[i]["response_length"][j]
+ correctness_j = data[i]["correctness"][j]
+
+ enc = policy_tokenizer(full_j, add_special_tokens=False)
+ token_ids = enc["input_ids"]
+
+ n_k = max(1, int((length_j - 1) / reward_chunk_length + 1))
+ for k in range(n_k):
+ chunk_end = min(length_j, (k + 1) * reward_chunk_length)
+ raw_chunk_text = get_prefix_text_by_chunk_end(chunk_end, token_ids)
+
+ if config.model.model_base == "mmada":
+ pad_token = "<|endoftext|>"
+ else:
+ pad_token = "<|mdm_mask|>"
+ clean_chunk_text = raw_chunk_text.replace(pad_token, "").strip()
+ full_j = full_j.replace(pad_token, "").strip()
+
+ if not clean_chunk_text:
+ data[i]["reward_prompt"][j].append("")
+ # 直接填好 k_sample 个 0,后面不再改写这个位置
+ data[i]["process_reward_list"][j].append([0.0] * k_sample)
+ data[i]["process_reward_response"][j].append([""] * k_sample)
+ continue
+
+ prompt_text = get_prompt_text(
+ clean_chunk_text,
+ q_i,
+ correctness_j,
+ full_j,
+ data[i]["ground_truth_answer"],
+ )
+ data[i]["reward_prompt"][j].append(prompt_text)
+
+ msgs = build_reward_messages(abs_image, prompt_text)
+ messages_all.append(msgs)
+ index_list.append((i, j, k))
+
+ data[i]["process_reward_list"][j].append([])
+ data[i]["process_reward_response"][j].append([])
+
+ Nq = len(messages_all)
+ print(f"[Main] Total prompts: {Nq}", flush=True)
+
+ if Nq > 0:
+ idx_perm = list(range(Nq))
+ random.shuffle(idx_perm)
+ shuffled_msgs = [messages_all[t] for t in idx_perm]
+
+ cprint("start VLM generation...", "green")
+ flat_texts_shuffled = generate_results(
+ shuffled_msgs,
+ R=k_sample,
+ gpu_groups=gpu_groups,
+ task_queues=task_queues,
+ result_queues=result_queues,
+ )
+ cprint("generation job done!", "green")
+
+ expected = Nq * k_sample
+ if len(flat_texts_shuffled) != expected:
+ raise RuntimeError(
+ f"Unexpected #outputs: {len(flat_texts_shuffled)} vs {expected}"
+ )
+
+ flat_texts: List[str] = [None] * expected
+ for pos, orig_idx in enumerate(idx_perm):
+ start = pos * k_sample
+ flat_texts[
+ orig_idx * k_sample: orig_idx * k_sample + k_sample
+ ] = flat_texts_shuffled[start:start + k_sample]
+
+ p = 0
+ for t in range(Nq):
+ i1, j1, k1 = index_list[t]
+ scores: List[float] = []
+ responses: List[str] = [] # 新增:记录原始输出
+ for _ in range(k_sample):
+ out = flat_texts[p]
+ p += 1
+ responses.append(out)
+ scores.append(float(normalize_reward_from_response(out)))
+ data[i1]["process_reward_list"][j1][k1] = scores
+ data[i1]["process_reward_response"][j1][k1] = responses # 新增
+
+
+ if num_node > 1:
+ output_path = f"../{project_name}/temp_data/outputs-{node_index}-{outputs_name}.json"
+ else:
+ output_path = f"../{project_name}/temp_data/outputs-{outputs_name}.json"
+
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+ with open(output_path, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+ print(f"[Main] Saved to {output_path}", flush=True)
+
+ stop_workers(task_queues, processes)
+ print("[Main] Workers stopped.", flush=True)