GuoruiSong commited on
Commit
b50f36e
·
verified ·
1 Parent(s): 867b992

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_clevr.sh +19 -0
  2. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_geoqa8k.sh +19 -0
  3. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/config.yaml +96 -0
  4. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/caption_format.jinja +1 -0
  5. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/math_format.jinja +1 -0
  6. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/r1v_format.jinja +1 -0
  7. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/llama3.2v_critic_grpo.sh +29 -0
  8. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_7b_llava_critic_grpo.sh +19 -0
  9. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_rl_7b_llava_critic_grpo.sh +19 -0
  10. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/qwen2_5_vl_7b_llava_critic_grpo.sh +19 -0
  11. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/caption.py +46 -0
  12. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/math.py +46 -0
  13. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/r1v.py +47 -0
  14. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/runtime_env.yaml +9 -0
  15. LLaVA-NeXT/llava-critic-r1/EasyR1/examples/thinklite_vl_7b_llava_critic_grpo.sh +19 -0
  16. LLaVA-NeXT/llava-critic-r1/EasyR1/verl.egg-info/PKG-INFO +262 -0
  17. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Dockerfile +76 -0
  18. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Makefile +18 -0
  19. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/README.md +217 -0
  20. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/__init__.py +32 -0
  21. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/init +1 -0
  22. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/__init__.py +13 -0
  23. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/monkey_patch.py +32 -0
  24. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/__init__.py +13 -0
  25. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/flash_attention_utils.py +191 -0
  26. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/qwen2_vl.py +189 -0
  27. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/protocol.py +689 -0
  28. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/pyproject.toml +39 -0
  29. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/requirements.txt +19 -0
  30. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/__init__.py +13 -0
  31. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/__init__.py +19 -0
  32. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/decorator.py +213 -0
  33. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/__init__.py +13 -0
  34. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/ray.py +28 -0
  35. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker.py +202 -0
  36. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker_group.py +194 -0
  37. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/__init__.py +18 -0
  38. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/base.py +493 -0
  39. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/__init__.py +13 -0
  40. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/config.py +123 -0
  41. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/core_algos.py +428 -0
  42. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/data_loader.py +88 -0
  43. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/main.py +130 -0
  44. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/metrics.py +120 -0
  45. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/ray_trainer.py +637 -0
  46. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/__init__.py +13 -0
  47. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/__init__.py +18 -0
  48. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/checkpoint_manager.py +160 -0
  49. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/fsdp_checkpoint_manager.py +113 -0
  50. LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/dataset.py +235 -0
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_clevr.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ set -x
4
+
5
+ export PYTHONUNBUFFERED=1
6
+
7
+ MODEL_PATH=Qwen/Qwen2.5-VL-3B-Instruct # replace it with your local file path
8
+
9
+ python3 -m verl.trainer.main \
10
+ config=examples/config.yaml \
11
+ data.train_files=BUAADreamer/clevr_count_70k@train \
12
+ data.val_files=BUAADreamer/clevr_count_70k@test \
13
+ data.format_prompt=./examples/format_prompt/r1v_format.jinja \
14
+ worker.actor.model.model_path=${MODEL_PATH} \
15
+ worker.rollout.tensor_parallel_size=1 \
16
+ worker.reward.reward_type=sequential \
17
+ worker.reward.reward_function=./examples/reward_function/r1v.py:compute_score \
18
+ trainer.experiment_name=qwen2_5_vl_3b_clevr \
19
+ trainer.n_gpus_per_node=2
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/baselines/qwen2_5_vl_3b_geoqa8k.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ set -x
4
+
5
+ export PYTHONUNBUFFERED=1
6
+
7
+ MODEL_PATH=Qwen/Qwen2.5-VL-3B-Instruct # replace it with your local file path
8
+
9
+ python3 -m verl.trainer.main \
10
+ config=examples/config.yaml \
11
+ data.train_files=leonardPKU/GEOQA_8K_R1V@train \
12
+ data.val_files=leonardPKU/GEOQA_8K_R1V@test \
13
+ data.format_prompt=./examples/format_prompt/r1v_format.jinja \
14
+ worker.actor.model.model_path=${MODEL_PATH} \
15
+ worker.rollout.tensor_parallel_size=1 \
16
+ worker.reward.reward_type=sequential \
17
+ worker.reward.reward_function=./examples/reward_function/r1v.py:compute_score \
18
+ trainer.experiment_name=qwen2_5_vl_3b_geoqa8k \
19
+ trainer.n_gpus_per_node=8
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/config.yaml ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data:
2
+ train_files: hiyouga/math12k@train
3
+ val_files: hiyouga/math12k@test
4
+ prompt_key: problem
5
+ answer_key: answer
6
+ image_key: image
7
+ max_prompt_length: 4096
8
+ max_response_length: 2048
9
+ rollout_batch_size: 256
10
+ val_batch_size: 1024
11
+ format_prompt: ./examples/format_prompt/math_format.jinja
12
+ override_chat_template: null
13
+ shuffle: true
14
+ seed: 1
15
+ max_pixels: 4194304
16
+ min_pixels: 262144
17
+ filter_overlong_prompts: true
18
+
19
+ algorithm:
20
+ adv_estimator: grpo
21
+ disable_kl: false
22
+ use_kl_loss: true
23
+ kl_penalty: low_var_kl
24
+ kl_coef: 1.0e-2
25
+
26
+ worker:
27
+ actor:
28
+ global_batch_size: 128
29
+ micro_batch_size_per_device_for_update: 4
30
+ micro_batch_size_per_device_for_experience: 8
31
+ max_grad_norm: 1.0
32
+ padding_free: true
33
+ ulysses_sequence_parallel_size: 1
34
+ is_entropy_reg: False
35
+ entropy_coef: 0.001
36
+ model:
37
+ model_path: Qwen/Qwen2.5-7B-Instruct
38
+ enable_gradient_checkpointing: true
39
+ trust_remote_code: true
40
+ freeze_vision_tower: false
41
+ optim:
42
+ lr: 1.0e-6
43
+ weight_decay: 1.0e-2
44
+ strategy: adamw # {adamw, adamw_bf16}
45
+ lr_warmup_ratio: 0.0
46
+ fsdp:
47
+ enable_full_shard: true
48
+ enable_cpu_offload: false
49
+ enable_rank0_init: true
50
+ offload:
51
+ offload_params: true # true: more CPU memory; false: more GPU memory
52
+ offload_optimizer: true # true: more CPU memory; false: more GPU memory
53
+
54
+ rollout:
55
+ n: 8
56
+ temperature: 1.0
57
+ top_p: 0.99
58
+ gpu_memory_utilization: 0.6
59
+ max_num_batched_tokens: 8192
60
+ max_model_len: 8192
61
+ enforce_eager: false
62
+ enable_chunked_prefill: false
63
+ tensor_parallel_size: 2
64
+ limit_images: 10
65
+ val_override_config:
66
+ temperature: 0.5
67
+ n: 1
68
+
69
+ ref:
70
+ fsdp:
71
+ enable_full_shard: true
72
+ enable_cpu_offload: true # true: more CPU memory; false: more GPU memory
73
+ enable_rank0_init: true
74
+ offload:
75
+ offload_params: false
76
+
77
+ reward:
78
+ reward_type: batch
79
+ reward_function: ./examples/reward_function/math.py:compute_score
80
+
81
+ trainer:
82
+ total_epochs: 15
83
+ max_steps: null
84
+ project_name: easy_r1
85
+ experiment_name: qwen2_5_7b_math_grpo
86
+ logger: ["console", "wandb"]
87
+ nnodes: 1
88
+ n_gpus_per_node: 8
89
+ val_freq: -1 # -1 to disable
90
+ val_before_train: false
91
+ val_only: false
92
+ val_generations_to_log: 3
93
+ save_freq: 10 # -1 to disable
94
+ save_limit: -1 # -1 to disable
95
+ save_checkpoint_path: null
96
+ load_checkpoint_path: null
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/caption_format.jinja ADDED
@@ -0,0 +1 @@
 
 
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 <caption> </caption> tags. The reasoning process MUST BE enclosed within <think> </think> tags. The final answer MUST BE put in \boxed{}.
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/math_format.jinja ADDED
@@ -0,0 +1 @@
 
 
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 <think> </think> tags. The final answer MUST BE put in \boxed{}.
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/format_prompt/r1v_format.jinja ADDED
@@ -0,0 +1 @@
 
 
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 <think> </think> and <answer> </answer> tags, respectively, i.e., <think> reasoning process here </think><answer> answer here </answer>
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/llama3.2v_critic_grpo.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ set -x
4
+
5
+ export PYTHONUNBUFFERED=1
6
+
7
+ MODEL_PATH=meta-llama/Llama-3.2-11B-Vision-Instruct # replace it with your local file path
8
+
9
+ python3 -m verl.trainer.main \
10
+ config=examples/config.yaml \
11
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
12
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
13
+ data.shuffle=True \
14
+ data.format_prompt=./examples/format_prompt/math_format.jinja \
15
+ data.rollout_batch_size=256 \
16
+ data.max_prompt_length=4096 \
17
+ data.prompt_key=question \
18
+ worker.actor.is_entropy_reg=False \
19
+ worker.actor.global_batch_size=128 \
20
+ worker.actor.optim.lr=5.0e-7 \
21
+ worker.actor.micro_batch_size_per_device_for_update=4 \
22
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
23
+ worker.actor.model.model_path=${MODEL_PATH} \
24
+ worker.rollout.tensor_parallel_size=8 \
25
+ worker.rollout.limit_images=1 \
26
+ worker.rollout.gpu_memory_utilization=0.6 \
27
+ worker.reward.reward_function=./examples/reward_function/math.py:compute_score \
28
+ trainer.experiment_name=llama3.2v_critic_grpo \
29
+ trainer.n_gpus_per_node=8
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_7b_llava_critic_grpo.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ MODEL_PATH=XiaomiMiMo/MiMo-VL-7B-SFT-2508
4
+
5
+ python3 -m verl.trainer.main \
6
+ config=examples/config.yaml \
7
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
8
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
9
+ data.filter_overlong_prompts=True \
10
+ data.prompt_key=question \
11
+ data.max_prompt_length=6142 \
12
+ worker.actor.is_entropy_reg=False \
13
+ worker.actor.global_batch_size=128 \
14
+ worker.actor.micro_batch_size_per_device_for_update=4 \
15
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
16
+ worker.actor.model.model_path=${MODEL_PATH} \
17
+ trainer.experiment_name=mimo_vl_7b_llava_critic_grpo \
18
+ trainer.n_gpus_per_node=8
19
+
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/mimo_vl_rl_7b_llava_critic_grpo.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ MODEL_PATH=XiaomiMiMo/MiMo-VL-7B-RL-2508
4
+
5
+ python3 -m verl.trainer.main \
6
+ config=examples/config.yaml \
7
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
8
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
9
+ data.filter_overlong_prompts=True \
10
+ data.prompt_key=question \
11
+ data.max_prompt_length=6142 \
12
+ worker.actor.is_entropy_reg=False \
13
+ worker.actor.global_batch_size=128 \
14
+ worker.actor.micro_batch_size_per_device_for_update=4 \
15
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
16
+ worker.actor.model.model_path=${MODEL_PATH} \
17
+ trainer.experiment_name=mimo_vl_rl_7b_llava_critic_grpo \
18
+ trainer.n_gpus_per_node=8
19
+
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/qwen2_5_vl_7b_llava_critic_grpo.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ MODEL_PATH=Qwen/Qwen2.5-VL-7B-Instruct
4
+
5
+ python3 -m verl.trainer.main \
6
+ config=examples/config.yaml \
7
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
8
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
9
+ data.filter_overlong_prompts=True \
10
+ data.prompt_key=question \
11
+ data.max_prompt_length=6142 \
12
+ worker.actor.is_entropy_reg=False \
13
+ worker.actor.global_batch_size=128 \
14
+ worker.actor.micro_batch_size_per_device_for_update=4 \
15
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
16
+ worker.actor.model.model_path=${MODEL_PATH} \
17
+ trainer.experiment_name=qwen2_5_vl_7b_llava_critic_grpo \
18
+ trainer.n_gpus_per_node=8
19
+
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/caption.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from typing import Dict, List
17
+
18
+ from mathruler.grader import extract_boxed_content, grade_answer
19
+
20
+
21
+ def format_reward(predict: str) -> float:
22
+ pattern = re.compile(r"<caption>.*</caption>.*<think>.*</think>.*\\boxed\{.*\}.*", re.DOTALL)
23
+ format_match = re.fullmatch(pattern, predict)
24
+ return 1.0 if format_match else 0.0
25
+
26
+
27
+ def accuracy_reward(predict: str, ground_truth: str) -> float:
28
+ answer = extract_boxed_content(predict)
29
+ return 1.0 if grade_answer(answer, ground_truth) else 0.0
30
+
31
+
32
+ def compute_score(predicts: List[str], ground_truths: List[str], format_weight: float = 0.1) -> List[Dict[str, float]]:
33
+ scores = []
34
+ for predict, ground_truth in zip(predicts, ground_truths):
35
+ predict = re.sub(r"\s*(<|>|/)\s*", r"\1", predict) # handle qwen2.5vl-32b format
36
+ format_score = format_reward(predict)
37
+ accuracy_score = accuracy_reward(predict, ground_truth)
38
+ scores.append(
39
+ {
40
+ "overall": (1 - format_weight) * accuracy_score + format_weight * format_score,
41
+ "format": format_score,
42
+ "accuracy": accuracy_score,
43
+ }
44
+ )
45
+
46
+ return scores
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/math.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from typing import Dict, List
17
+
18
+ from mathruler.grader import extract_boxed_content, grade_answer
19
+
20
+
21
+ def format_reward(predict: str) -> float:
22
+ pattern = re.compile(r"<think>.*</think>.*\\boxed\{.*\}.*", re.DOTALL)
23
+ format_match = re.fullmatch(pattern, predict)
24
+ return 1.0 if format_match else 0.0
25
+
26
+
27
+ def accuracy_reward(predict: str, ground_truth: str) -> float:
28
+ answer = extract_boxed_content(predict)
29
+ return 1.0 if grade_answer(answer, ground_truth) else 0.0
30
+
31
+
32
+ def compute_score(predicts: List[str], ground_truths: List[str], format_weight: float = 0.1) -> List[Dict[str, float]]:
33
+ scores = []
34
+ for predict, ground_truth in zip(predicts, ground_truths):
35
+ predict = re.sub(r"\s*(<|>|/)\s*", r"\1", predict) # handle qwen2.5vl-32b format
36
+ format_score = format_reward(predict)
37
+ accuracy_score = accuracy_reward(predict, ground_truth)
38
+ scores.append(
39
+ {
40
+ "overall": (1 - format_weight) * accuracy_score + format_weight * format_score,
41
+ "format": format_score,
42
+ "accuracy": accuracy_score,
43
+ }
44
+ )
45
+
46
+ return scores
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/reward_function/r1v.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from typing import Dict
17
+
18
+ from mathruler.grader import grade_answer
19
+
20
+
21
+ def format_reward(predict: str) -> float:
22
+ pattern = re.compile(r"<think>.*?</think>\s*<answer>.*?</answer>", re.DOTALL)
23
+ format_match = re.fullmatch(pattern, predict)
24
+ return 1.0 if format_match else 0.0
25
+
26
+
27
+ def accuracy_reward(predict: str, ground_truth: str) -> float:
28
+ try:
29
+ content_match = re.search(r"<answer>(.*?)</answer>", predict)
30
+ given_answer = content_match.group(1).strip() if content_match else predict.strip()
31
+ if grade_answer(given_answer, ground_truth.strip()):
32
+ return 1.0
33
+
34
+ except Exception:
35
+ pass
36
+
37
+ return 0.0
38
+
39
+
40
+ def compute_score(predict: str, ground_truth: str, format_weight: float = 0.5) -> Dict[str, float]:
41
+ format_score = format_reward(predict)
42
+ accuracy_score = accuracy_reward(predict, ground_truth)
43
+ return {
44
+ "overall": (1 - format_weight) * accuracy_score + format_weight * format_score,
45
+ "format": format_score,
46
+ "accuracy": accuracy_score,
47
+ }
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/runtime_env.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ working_dir: ./
2
+ excludes: ["/.git/"]
3
+ env_vars:
4
+ TOKENIZERS_PARALLELISM: "true"
5
+ NCCL_DEBUG: "WARN"
6
+ VLLM_LOGGING_LEVEL: "WARN"
7
+ TORCH_NCCL_AVOID_RECORD_STREAMS: "1"
8
+ PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:False"
9
+ PYTHONUNBUFFERED: "1"
LLaVA-NeXT/llava-critic-r1/EasyR1/examples/thinklite_vl_7b_llava_critic_grpo.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ MODEL_PATH=russwang/ThinkLite-VL-7B
4
+
5
+ python3 -m verl.trainer.main \
6
+ config=examples/config.yaml \
7
+ data.train_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
8
+ data.val_files=lmms-lab/LLaVA-Critic-GRPO-dataset@train \
9
+ data.filter_overlong_prompts=True \
10
+ data.prompt_key=question \
11
+ data.max_prompt_length=6142 \
12
+ worker.actor.is_entropy_reg=False \
13
+ worker.actor.global_batch_size=128 \
14
+ worker.actor.optim.lr=5.0e-7 \
15
+ worker.actor.micro_batch_size_per_device_for_update=4 \
16
+ worker.actor.micro_batch_size_per_device_for_experience=8 \
17
+ worker.actor.model.model_path=${MODEL_PATH} \
18
+ trainer.experiment_name=thinklite_vl_7b_llava_critic_grpo \
19
+ trainer.n_gpus_per_node=8
LLaVA-NeXT/llava-critic-r1/EasyR1/verl.egg-info/PKG-INFO ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: verl
3
+ Version: 0.3.1.dev0
4
+ Summary: An Efficient, Scalable, Multi-Modality RL Training Framework based on veRL
5
+ Home-page: https://github.com/volcengine/verl
6
+ Author: verl
7
+ Author-email: zhangchi.usc1992@bytedance.com, gmsheng@connect.hku.hk, hiyouga@buaa.edu.cn
8
+ License: Apache 2.0 License
9
+ Requires-Python: >=3.9.0
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: accelerate
13
+ Requires-Dist: codetiming
14
+ Requires-Dist: datasets
15
+ Requires-Dist: liger-kernel
16
+ Requires-Dist: mathruler
17
+ Requires-Dist: numpy
18
+ Requires-Dist: omegaconf
19
+ Requires-Dist: pandas
20
+ Requires-Dist: peft
21
+ Requires-Dist: pillow
22
+ Requires-Dist: pyarrow>=15.0.0
23
+ Requires-Dist: pylatexenc
24
+ Requires-Dist: qwen-vl-utils
25
+ Requires-Dist: ray[default]
26
+ Requires-Dist: tensordict
27
+ Requires-Dist: torchdata
28
+ Requires-Dist: transformers>=4.51.0
29
+ Requires-Dist: vllm>=0.7.3
30
+ Requires-Dist: wandb
31
+ Provides-Extra: dev
32
+ Requires-Dist: pre-commit; extra == "dev"
33
+ Requires-Dist: ruff; extra == "dev"
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: description
37
+ Dynamic: description-content-type
38
+ Dynamic: home-page
39
+ Dynamic: license
40
+ Dynamic: license-file
41
+ Dynamic: provides-extra
42
+ Dynamic: requires-dist
43
+ Dynamic: requires-python
44
+ Dynamic: summary
45
+
46
+ # EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework
47
+
48
+ [![GitHub Repo stars](https://img.shields.io/github/stars/hiyouga/EasyR1)](https://github.com/hiyouga/EasyR1/stargazers)
49
+ [![Twitter](https://img.shields.io/twitter/follow/llamafactory_ai)](https://twitter.com/llamafactory_ai)
50
+
51
+ 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.
52
+
53
+ 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.
54
+
55
+ ## Features
56
+
57
+ - Supported models
58
+ - Llama3/Qwen2/Qwen2.5/Qwen3 language models
59
+ - Qwen2/Qwen2.5-VL vision language models
60
+ - DeepSeek-R1 distill models
61
+
62
+ - Supported algorithms
63
+ - GRPO
64
+ - Reinforce++
65
+ - ReMax
66
+ - RLOO
67
+
68
+ - Supported datasets
69
+ - Any text, vision-text dataset in a [specific format](#custom-dataset)
70
+
71
+ - Supported tricks
72
+ - Padding-free training
73
+ - Resuming from checkpoint
74
+ - Wandb & SwanLab & Mlflow & Tensorboard tracking
75
+
76
+ ## Requirements
77
+
78
+ ### Software Requirements
79
+
80
+ - Python 3.9+
81
+ - transformers>=4.51.0
82
+ - flash-attn>=2.4.3
83
+ - vllm>=0.8.3
84
+
85
+ We provide a [Dockerfile](./Dockerfile) to easily build environments.
86
+
87
+ We recommend using the [pre-built docker image](https://hub.docker.com/r/hiyouga/verl) in EasyR1.
88
+
89
+ ```bash
90
+ docker pull hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0
91
+ ```
92
+
93
+ ### Hardware Requirements
94
+
95
+ \* *estimated*
96
+
97
+ | Method | Bits | 1.5B | 3B | 7B | 32B |
98
+ | ------------------------ | ---- | ------ | ------ | ------ | ------- |
99
+ | GRPO Full Fine-Tuning | AMP | 2*24GB | 4*40GB | 8*40GB | 16*80GB |
100
+ | GRPO Full Fine-Tuning | BF16 | 1*24GB | 1*40GB | 4*40GB | 8*80GB |
101
+
102
+ > [!NOTE]
103
+ > Use `worker.actor.fsdp.torch_dtype=bf16` and `worker.actor.optim.strategy=adamw_bf16` to enable bf16 training.
104
+ >
105
+ > We are working hard to reduce the VRAM in RL training, LoRA support will be integrated in next updates.
106
+
107
+ ## Tutorial: Run Qwen2.5-VL GRPO on [Geometry3K](https://huggingface.co/datasets/hiyouga/geometry3k) Dataset in Just 3 Steps
108
+
109
+ ![image](assets/qwen2_5_vl_7b_geo.png)
110
+
111
+ ### Installation
112
+
113
+ ```bash
114
+ git clone https://github.com/hiyouga/EasyR1.git
115
+ cd EasyR1
116
+ pip install -e .
117
+ ```
118
+
119
+ ### GRPO Training
120
+
121
+ ```bash
122
+ bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
123
+ ```
124
+
125
+ ### Merge Checkpoint in Hugging Face Format
126
+
127
+ ```bash
128
+ python3 scripts/model_merger.py --local_dir checkpoints/easy_r1/exp_name/global_step_1/actor
129
+ ```
130
+
131
+ > [!TIP]
132
+ > If you encounter issues with connecting to Hugging Face, consider using `export HF_ENDPOINT=https://hf-mirror.com`.
133
+ >
134
+ > If you want to use SwanLab logger, consider using `bash examples/qwen2_5_vl_7b_geo3k_swanlab.sh`.
135
+
136
+ ## Custom Dataset
137
+
138
+ Please refer to the example datasets to prepare your own dataset.
139
+
140
+ - Text dataset: https://huggingface.co/datasets/hiyouga/math12k
141
+ - Image-text dataset: https://huggingface.co/datasets/hiyouga/geometry3k
142
+ - Multi-image-text dataset: https://huggingface.co/datasets/hiyouga/journeybench-multi-image-vqa
143
+
144
+ ## How to Understand GRPO in EasyR1
145
+
146
+ ![image](assets/easyr1_grpo.png)
147
+
148
+ - 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).
149
+
150
+ ## How to Run 70B+ Model in Multi-node Environment
151
+
152
+ 1. Start the Ray head node.
153
+
154
+ ```bash
155
+ ray start --head --port=6379 --dashboard-host=0.0.0.0
156
+ ```
157
+
158
+ 2. Start the Ray worker node and connect to the head node.
159
+
160
+ ```bash
161
+ ray start --address=<head_node_ip>:6379
162
+ ```
163
+
164
+ 3. Check the Ray resource pool.
165
+
166
+ ```bash
167
+ ray status
168
+ ```
169
+
170
+ 4. Run training script on the Ray head node only.
171
+
172
+ ```bash
173
+ bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
174
+ ```
175
+
176
+ 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.
177
+
178
+ ## Other Baselines
179
+
180
+ We also reproduced the following two baselines of the [R1-V](https://github.com/deep-agent/R1-V) project.
181
+ - [CLEVR-70k-Counting](examples/baselines/qwen2_5_vl_3b_clevr.sh): Train the Qwen2.5-VL-3B-Instruct model on counting problem.
182
+ - [GeoQA-8k](examples/baselines/qwen2_5_vl_3b_geoqa8k.sh): Train the Qwen2.5-VL-3B-Instruct model on GeoQA problem.
183
+
184
+ ## Performance Baselines
185
+
186
+ See [baselines.md](assets/baselines.md).
187
+
188
+ ## Awesome Work using EasyR1
189
+
190
+ - **MMR1**: Advancing the Frontiers of Multimodal Reasoning. [![[code]](https://img.shields.io/github/stars/LengSicong/MMR1)](https://github.com/LengSicong/MMR1)
191
+ - **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)
192
+ - **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)
193
+ - **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)
194
+ - **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)
195
+ - **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)
196
+ - **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)
197
+ - **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)
198
+ - **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)
199
+ - **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)
200
+ - **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)
201
+ - **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)
202
+ - **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)
203
+ - **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)
204
+
205
+ ## TODO
206
+
207
+ - Support LoRA (high priority).
208
+ - Support ulysses parallelism for VLMs (middle priority).
209
+ - Support more VLM architectures.
210
+
211
+ > [!NOTE]
212
+ > 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).
213
+
214
+ ### Known bugs
215
+
216
+ These features are temporarily disabled for now, we plan to fix them one-by-one in the future updates.
217
+
218
+ - Vision language models are not compatible with ulysses parallelism yet.
219
+
220
+ ## Discussion Group
221
+
222
+ 👋 Join our [WeChat group](assets/wechat.jpg).
223
+
224
+ ## FAQs
225
+
226
+ > ValueError: Image features and image tokens do not match: tokens: 8192, features 9800
227
+
228
+ Increase the `data.max_prompt_length` or reduce the `data.max_pixels`.
229
+
230
+ > RuntimeError: CUDA Error: out of memory at /workspace/csrc/cumem_allocator.cpp:62
231
+
232
+ Reduce the `worker.rollout.gpu_memory_utilization` and enable `worker.actor.offload.offload_params`.
233
+
234
+ > RuntimeError: 0 active drivers ([]). There should only be one.
235
+
236
+ Uninstall `deepspeed` from the current python environment.
237
+
238
+ ## Citation
239
+
240
+ 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
241
+
242
+ We also thank Guangming Sheng and Chi Zhang for helpful discussions.
243
+
244
+ ```bibtex
245
+ @misc{zheng2025easyr1,
246
+ title = {EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework},
247
+ author = {Yaowei Zheng, Junting Lu, Shenzhi Wang, Zhangchi Feng, Dongdong Kuang, Yuwen Xiong},
248
+ howpublished = {\url{https://github.com/hiyouga/EasyR1}},
249
+ year = {2025}
250
+ }
251
+ ```
252
+
253
+ We recommend to also cite the original work.
254
+
255
+ ```bibtex
256
+ @article{sheng2024hybridflow,
257
+ title = {HybridFlow: A Flexible and Efficient RLHF Framework},
258
+ 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},
259
+ year = {2024},
260
+ journal = {arXiv preprint arXiv: 2409.19256}
261
+ }
262
+ ```
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Dockerfile ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Start from the NVIDIA official image (ubuntu-22.04 + cuda-12.6 + python-3.10)
2
+ # https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/rel-24-08.html
3
+ FROM nvcr.io/nvidia/pytorch:24.08-py3
4
+
5
+ # Define environments
6
+ ENV MAX_JOBS=32
7
+ ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
8
+ ENV DEBIAN_FRONTEND=noninteractive
9
+ ENV NODE_OPTIONS=""
10
+ ENV PIP_ROOT_USER_ACTION=ignore
11
+ ENV HF_HUB_ENABLE_HF_TRANSFER="1"
12
+
13
+ # Define installation arguments
14
+ ARG APT_SOURCE=https://mirrors.tuna.tsinghua.edu.cn/ubuntu/
15
+ ARG PIP_INDEX=https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
16
+
17
+ # Set apt source
18
+ RUN cp /etc/apt/sources.list /etc/apt/sources.list.bak && \
19
+ { \
20
+ echo "deb ${APT_SOURCE} jammy main restricted universe multiverse"; \
21
+ echo "deb ${APT_SOURCE} jammy-updates main restricted universe multiverse"; \
22
+ echo "deb ${APT_SOURCE} jammy-backports main restricted universe multiverse"; \
23
+ echo "deb ${APT_SOURCE} jammy-security main restricted universe multiverse"; \
24
+ } > /etc/apt/sources.list
25
+
26
+ # Install systemctl
27
+ RUN apt-get update && \
28
+ apt-get install -y -o Dpkg::Options::="--force-confdef" systemd && \
29
+ apt-get clean
30
+
31
+ # Install tini
32
+ RUN apt-get update && \
33
+ apt-get install -y tini && \
34
+ apt-get clean
35
+
36
+ # Change pip source
37
+ RUN pip config set global.index-url "${PIP_INDEX}" && \
38
+ pip config set global.extra-index-url "${PIP_INDEX}" && \
39
+ python -m pip install --upgrade pip
40
+
41
+ # Uninstall nv-pytorch fork
42
+ RUN pip uninstall -y torch torchvision torchaudio \
43
+ pytorch-quantization pytorch-triton torch-tensorrt \
44
+ transformer_engine flash_attn apex megatron-core \
45
+ xgboost opencv grpcio
46
+
47
+ # Fix cv2
48
+ RUN rm -rf /usr/local/lib/python3.10/dist-packages/cv2
49
+
50
+ # Install torch-2.6.0+cu124 + vllm-0.8.4
51
+ # torch-2.6.0+cu124: cxx11abi=False
52
+ # torch-2.6.0+cu126: cxx11abi=True
53
+ # see https://github.com/flashinfer-ai/flashinfer/issues/911
54
+ RUN pip install --no-cache-dir "vllm==0.8.4" "torch==2.6.0" "torchvision==0.21.0" "torchaudio==2.6.0" tensordict torchdata \
55
+ "transformers[hf_xet]>=4.51.0" accelerate datasets peft hf-transfer \
56
+ "numpy<2.0.0" "pyarrow>=15.0.0" pandas \
57
+ ray[default] codetiming hydra-core pylatexenc qwen-vl-utils wandb liger-kernel mathruler \
58
+ pytest yapf py-spy pyext pre-commit ruff
59
+
60
+ # Install flash-attn-2.7.4.post1 (cxx11abi=False)
61
+ 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 && \
62
+ pip install --no-cache-dir flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
63
+
64
+ # Install flashinfer-0.2.2.post1+cu124 (cxx11abi=False)
65
+ # vllm-0.8.3 does not support flashinfer>=0.2.3
66
+ # see https://github.com/vllm-project/vllm/pull/15777
67
+ 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 && \
68
+ pip install --no-cache-dir flashinfer_python-0.2.2.post1+cu124torch2.6-cp38-abi3-linux_x86_64.whl
69
+
70
+ # Fix packages
71
+ RUN pip uninstall -y pynvml nvidia-ml-py && \
72
+ 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"
73
+
74
+ # Reset pip config
75
+ RUN pip config unset global.index-url && \
76
+ pip config unset global.extra-index-url
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/Makefile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: build commit quality style
2
+
3
+ check_dirs := examples scripts verl setup.py
4
+
5
+ build:
6
+ python3 setup.py sdist bdist_wheel
7
+
8
+ commit:
9
+ pre-commit install
10
+ pre-commit run --all-files
11
+
12
+ quality:
13
+ ruff check $(check_dirs)
14
+ ruff format --check $(check_dirs)
15
+
16
+ style:
17
+ ruff check $(check_dirs) --fix
18
+ ruff format $(check_dirs)
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/README.md ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework
2
+
3
+ [![GitHub Repo stars](https://img.shields.io/github/stars/hiyouga/EasyR1)](https://github.com/hiyouga/EasyR1/stargazers)
4
+ [![Twitter](https://img.shields.io/twitter/follow/llamafactory_ai)](https://twitter.com/llamafactory_ai)
5
+
6
+ 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.
7
+
8
+ 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.
9
+
10
+ ## Features
11
+
12
+ - Supported models
13
+ - Llama3/Qwen2/Qwen2.5/Qwen3 language models
14
+ - Qwen2/Qwen2.5-VL vision language models
15
+ - DeepSeek-R1 distill models
16
+
17
+ - Supported algorithms
18
+ - GRPO
19
+ - Reinforce++
20
+ - ReMax
21
+ - RLOO
22
+
23
+ - Supported datasets
24
+ - Any text, vision-text dataset in a [specific format](#custom-dataset)
25
+
26
+ - Supported tricks
27
+ - Padding-free training
28
+ - Resuming from checkpoint
29
+ - Wandb & SwanLab & Mlflow & Tensorboard tracking
30
+
31
+ ## Requirements
32
+
33
+ ### Software Requirements
34
+
35
+ - Python 3.9+
36
+ - transformers>=4.51.0
37
+ - flash-attn>=2.4.3
38
+ - vllm>=0.8.3
39
+
40
+ We provide a [Dockerfile](./Dockerfile) to easily build environments.
41
+
42
+ We recommend using the [pre-built docker image](https://hub.docker.com/r/hiyouga/verl) in EasyR1.
43
+
44
+ ```bash
45
+ docker pull hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0
46
+ ```
47
+
48
+ ### Hardware Requirements
49
+
50
+ \* *estimated*
51
+
52
+ | Method | Bits | 1.5B | 3B | 7B | 32B |
53
+ | ------------------------ | ---- | ------ | ------ | ------ | ------- |
54
+ | GRPO Full Fine-Tuning | AMP | 2*24GB | 4*40GB | 8*40GB | 16*80GB |
55
+ | GRPO Full Fine-Tuning | BF16 | 1*24GB | 1*40GB | 4*40GB | 8*80GB |
56
+
57
+ > [!NOTE]
58
+ > Use `worker.actor.fsdp.torch_dtype=bf16` and `worker.actor.optim.strategy=adamw_bf16` to enable bf16 training.
59
+ >
60
+ > We are working hard to reduce the VRAM in RL training, LoRA support will be integrated in next updates.
61
+
62
+ ## Tutorial: Run Qwen2.5-VL GRPO on [Geometry3K](https://huggingface.co/datasets/hiyouga/geometry3k) Dataset in Just 3 Steps
63
+
64
+ ![image](assets/qwen2_5_vl_7b_geo.png)
65
+
66
+ ### Installation
67
+
68
+ ```bash
69
+ git clone https://github.com/hiyouga/EasyR1.git
70
+ cd EasyR1
71
+ pip install -e .
72
+ ```
73
+
74
+ ### GRPO Training
75
+
76
+ ```bash
77
+ bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
78
+ ```
79
+
80
+ ### Merge Checkpoint in Hugging Face Format
81
+
82
+ ```bash
83
+ python3 scripts/model_merger.py --local_dir checkpoints/easy_r1/exp_name/global_step_1/actor
84
+ ```
85
+
86
+ > [!TIP]
87
+ > If you encounter issues with connecting to Hugging Face, consider using `export HF_ENDPOINT=https://hf-mirror.com`.
88
+ >
89
+ > If you want to use SwanLab logger, consider using `bash examples/qwen2_5_vl_7b_geo3k_swanlab.sh`.
90
+
91
+ ## Custom Dataset
92
+
93
+ Please refer to the example datasets to prepare your own dataset.
94
+
95
+ - Text dataset: https://huggingface.co/datasets/hiyouga/math12k
96
+ - Image-text dataset: https://huggingface.co/datasets/hiyouga/geometry3k
97
+ - Multi-image-text dataset: https://huggingface.co/datasets/hiyouga/journeybench-multi-image-vqa
98
+
99
+ ## How to Understand GRPO in EasyR1
100
+
101
+ ![image](assets/easyr1_grpo.png)
102
+
103
+ - 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).
104
+
105
+ ## How to Run 70B+ Model in Multi-node Environment
106
+
107
+ 1. Start the Ray head node.
108
+
109
+ ```bash
110
+ ray start --head --port=6379 --dashboard-host=0.0.0.0
111
+ ```
112
+
113
+ 2. Start the Ray worker node and connect to the head node.
114
+
115
+ ```bash
116
+ ray start --address=<head_node_ip>:6379
117
+ ```
118
+
119
+ 3. Check the Ray resource pool.
120
+
121
+ ```bash
122
+ ray status
123
+ ```
124
+
125
+ 4. Run training script on the Ray head node only.
126
+
127
+ ```bash
128
+ bash examples/qwen2_5_vl_7b_geo3k_grpo.sh
129
+ ```
130
+
131
+ 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.
132
+
133
+ ## Other Baselines
134
+
135
+ We also reproduced the following two baselines of the [R1-V](https://github.com/deep-agent/R1-V) project.
136
+ - [CLEVR-70k-Counting](examples/baselines/qwen2_5_vl_3b_clevr.sh): Train the Qwen2.5-VL-3B-Instruct model on counting problem.
137
+ - [GeoQA-8k](examples/baselines/qwen2_5_vl_3b_geoqa8k.sh): Train the Qwen2.5-VL-3B-Instruct model on GeoQA problem.
138
+
139
+ ## Performance Baselines
140
+
141
+ See [baselines.md](assets/baselines.md).
142
+
143
+ ## Awesome Work using EasyR1
144
+
145
+ - **MMR1**: Advancing the Frontiers of Multimodal Reasoning. [![[code]](https://img.shields.io/github/stars/LengSicong/MMR1)](https://github.com/LengSicong/MMR1)
146
+ - **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)
147
+ - **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)
148
+ - **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)
149
+ - **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)
150
+ - **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)
151
+ - **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)
152
+ - **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)
153
+ - **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)
154
+ - **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)
155
+ - **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)
156
+ - **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)
157
+ - **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)
158
+ - **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)
159
+
160
+ ## TODO
161
+
162
+ - Support LoRA (high priority).
163
+ - Support ulysses parallelism for VLMs (middle priority).
164
+ - Support more VLM architectures.
165
+
166
+ > [!NOTE]
167
+ > 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).
168
+
169
+ ### Known bugs
170
+
171
+ These features are temporarily disabled for now, we plan to fix them one-by-one in the future updates.
172
+
173
+ - Vision language models are not compatible with ulysses parallelism yet.
174
+
175
+ ## Discussion Group
176
+
177
+ 👋 Join our [WeChat group](assets/wechat.jpg).
178
+
179
+ ## FAQs
180
+
181
+ > ValueError: Image features and image tokens do not match: tokens: 8192, features 9800
182
+
183
+ Increase the `data.max_prompt_length` or reduce the `data.max_pixels`.
184
+
185
+ > RuntimeError: CUDA Error: out of memory at /workspace/csrc/cumem_allocator.cpp:62
186
+
187
+ Reduce the `worker.rollout.gpu_memory_utilization` and enable `worker.actor.offload.offload_params`.
188
+
189
+ > RuntimeError: 0 active drivers ([]). There should only be one.
190
+
191
+ Uninstall `deepspeed` from the current python environment.
192
+
193
+ ## Citation
194
+
195
+ 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
196
+
197
+ We also thank Guangming Sheng and Chi Zhang for helpful discussions.
198
+
199
+ ```bibtex
200
+ @misc{zheng2025easyr1,
201
+ title = {EasyR1: An Efficient, Scalable, Multi-Modality RL Training Framework},
202
+ author = {Yaowei Zheng, Junting Lu, Shenzhi Wang, Zhangchi Feng, Dongdong Kuang, Yuwen Xiong},
203
+ howpublished = {\url{https://github.com/hiyouga/EasyR1}},
204
+ year = {2025}
205
+ }
206
+ ```
207
+
208
+ We recommend to also cite the original work.
209
+
210
+ ```bibtex
211
+ @article{sheng2024hybridflow,
212
+ title = {HybridFlow: A Flexible and Efficient RLHF Framework},
213
+ 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},
214
+ year = {2024},
215
+ journal = {arXiv preprint arXiv: 2409.19256}
216
+ }
217
+ ```
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+
17
+ from .utils.py_functional import is_package_available
18
+
19
+
20
+ if is_package_available("modelscope"):
21
+ from modelscope.utils.hf_util import patch_hub # type: ignore
22
+
23
+
24
+ __version__ = "0.3.1.dev0"
25
+
26
+
27
+ if os.getenv("USE_MODELSCOPE_HUB", "0").lower() in ["true", "y", "1"]:
28
+ # Patch hub to download models from modelscope to speed up.
29
+ if not is_package_available("modelscope"):
30
+ raise ImportError("You are using the modelscope hub, please install modelscope by `pip install modelscope`.")
31
+
32
+ patch_hub()
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/init ADDED
@@ -0,0 +1 @@
 
 
1
+
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/monkey_patch.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
17
+
18
+ from .transformers.flash_attention_utils import flash_attention_forward
19
+ from .transformers.qwen2_vl import qwen2_vl_attn_forward
20
+
21
+
22
+ def apply_ulysses_patch(model_type: str) -> None:
23
+ if model_type in ("llama", "mllama", "gemma", "gemma2", "mistral", "qwen2", "qwen3", "qwen3_moe"):
24
+ ALL_ATTENTION_FUNCTIONS["flash_attention_2"] = flash_attention_forward
25
+ elif model_type in ("qwen2_vl", "qwen2_5_vl"):
26
+ from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import Qwen2_5_VLFlashAttention2
27
+ from transformers.models.qwen2_vl.modeling_qwen2_vl import Qwen2VLFlashAttention2
28
+
29
+ Qwen2VLFlashAttention2.forward = qwen2_vl_attn_forward
30
+ Qwen2_5_VLFlashAttention2.forward = qwen2_vl_attn_forward
31
+ else:
32
+ raise NotImplementedError(f"Model architecture {model_type} is not supported yet.")
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/flash_attention_utils.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The Fairseq Authors and the HuggingFace Inc. team
2
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
3
+ # Based on https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/modeling_flash_attention_utils.py
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import inspect
18
+ import os
19
+ from typing import Optional, Tuple
20
+
21
+ import torch
22
+ import torch.distributed as dist
23
+ from transformers.modeling_flash_attention_utils import _flash_attention_forward, fa_peft_integration_check
24
+ from transformers.utils import is_flash_attn_2_available, is_flash_attn_greater_or_equal_2_10
25
+
26
+ from ...utils.ulysses import (
27
+ gather_heads_scatter_seq,
28
+ gather_seq_scatter_heads,
29
+ get_ulysses_sequence_parallel_group,
30
+ get_ulysses_sequence_parallel_world_size,
31
+ )
32
+
33
+
34
+ if is_flash_attn_2_available():
35
+ from flash_attn import flash_attn_func, flash_attn_varlen_func
36
+
37
+ _flash_supports_window_size = "window_size" in inspect.signature(flash_attn_func).parameters
38
+ _flash_supports_deterministic = "deterministic" in inspect.signature(flash_attn_func).parameters
39
+ _flash_deterministic_enabled = os.environ.get("FLASH_ATTENTION_DETERMINISTIC", "0") == "1"
40
+ _flash_use_top_left_mask = not is_flash_attn_greater_or_equal_2_10()
41
+
42
+
43
+ def prepare_fa2_from_position_ids(
44
+ query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, position_ids: torch.Tensor
45
+ ):
46
+ query = query.view(-1, query.size(-2), query.size(-1))
47
+ key = key.contiguous().view(-1, key.size(-2), key.size(-1))
48
+ value = value.contiguous().view(-1, value.size(-2), value.size(-1))
49
+ position_ids = position_ids.flatten()
50
+ indices_q = torch.arange(position_ids.size(0), device=position_ids.device, dtype=torch.int32)
51
+ cu_seqlens = torch.cat(
52
+ (
53
+ indices_q[position_ids == 0],
54
+ torch.tensor(position_ids.size(), device=position_ids.device, dtype=torch.int32),
55
+ )
56
+ )
57
+ max_length = cu_seqlens.diff().max() # use cu_seqlens to infer max_length for qwen2vl mrope
58
+ return (query, key, value, indices_q, (cu_seqlens, cu_seqlens), (max_length, max_length))
59
+
60
+
61
+ def _custom_flash_attention_forward(
62
+ query_states: torch.Tensor,
63
+ key_states: torch.Tensor,
64
+ value_states: torch.Tensor,
65
+ attention_mask: Optional[torch.Tensor],
66
+ query_length: int,
67
+ is_causal: bool = True,
68
+ position_ids: Optional[torch.Tensor] = None,
69
+ sliding_window: Optional[int] = None,
70
+ use_top_left_mask: bool = False,
71
+ deterministic: Optional[bool] = None,
72
+ **kwargs,
73
+ ):
74
+ """
75
+ Patches flash attention forward to handle 3D position ids in mrope. (3, batch_size, seq_length)
76
+ """
77
+ if not use_top_left_mask:
78
+ causal = is_causal
79
+ else:
80
+ causal = is_causal and query_length != 1
81
+
82
+ # Assuming 4D tensors, key_states.shape[1] is the key/value sequence length (source length).
83
+ use_sliding_windows = (
84
+ _flash_supports_window_size and sliding_window is not None and key_states.shape[1] > sliding_window
85
+ )
86
+ flash_kwargs = {"window_size": (sliding_window, sliding_window)} if use_sliding_windows else {}
87
+
88
+ if _flash_supports_deterministic:
89
+ flash_kwargs["deterministic"] = deterministic if deterministic is not None else _flash_deterministic_enabled
90
+
91
+ if kwargs.get("softcap") is not None:
92
+ flash_kwargs["softcap"] = kwargs.pop("softcap")
93
+
94
+ query_states, key_states, value_states = fa_peft_integration_check(
95
+ query_states, key_states, value_states, target_dtype=torch.bfloat16
96
+ )
97
+
98
+ sp_size = get_ulysses_sequence_parallel_world_size()
99
+ if sp_size > 1:
100
+ # (batch_size, seq_length, num_head, head_size)
101
+ query_states = gather_seq_scatter_heads(query_states, seq_dim=1, head_dim=2)
102
+ key_states = gather_seq_scatter_heads(key_states, seq_dim=1, head_dim=2)
103
+ value_states = gather_seq_scatter_heads(value_states, seq_dim=1, head_dim=2)
104
+ position_ids_lst = [torch.empty_like(position_ids) for _ in range(sp_size)]
105
+ position_ids = dist.all_gather(position_ids_lst, position_ids, group=get_ulysses_sequence_parallel_group())
106
+ position_ids = torch.cat(position_ids_lst, dim=-1) # (..., batch_size, seq_length)
107
+
108
+ if position_ids is not None and position_ids.dim() == 3: # qwen2vl mrope
109
+ position_ids = position_ids[0]
110
+
111
+ if position_ids is not None and query_length != 1 and not (torch.diff(position_ids, dim=-1) >= 0).all():
112
+ batch_size = query_states.size(0)
113
+ query_states, key_states, value_states, _, cu_seq_lens, max_seq_lens = prepare_fa2_from_position_ids(
114
+ query_states, key_states, value_states, position_ids
115
+ )
116
+ cu_seqlens_q, cu_seqlens_k = cu_seq_lens
117
+ max_seqlen_in_batch_q, max_seqlen_in_batch_k = max_seq_lens
118
+ attn_output = flash_attn_varlen_func(
119
+ query_states,
120
+ key_states,
121
+ value_states,
122
+ cu_seqlens_q=cu_seqlens_q,
123
+ cu_seqlens_k=cu_seqlens_k,
124
+ max_seqlen_q=max_seqlen_in_batch_q,
125
+ max_seqlen_k=max_seqlen_in_batch_k,
126
+ dropout_p=kwargs.pop("dropout", 0.0),
127
+ softmax_scale=kwargs.pop("softmax_scale", None),
128
+ causal=causal,
129
+ **flash_kwargs,
130
+ )
131
+ attn_output = attn_output.view(batch_size, -1, attn_output.size(-2), attn_output.size(-1))
132
+ else:
133
+ attn_output = _flash_attention_forward(
134
+ query_states,
135
+ key_states,
136
+ value_states,
137
+ attention_mask,
138
+ query_length,
139
+ is_causal=is_causal,
140
+ sliding_window=sliding_window,
141
+ use_top_left_mask=use_top_left_mask,
142
+ deterministic=deterministic,
143
+ **kwargs,
144
+ ) # do not pass position_ids to old flash_attention_forward
145
+
146
+ if sp_size > 1:
147
+ # (batch_size, seq_length, num_head, head_size)
148
+ attn_output = gather_heads_scatter_seq(attn_output, head_dim=2, seq_dim=1)
149
+
150
+ return attn_output
151
+
152
+
153
+ def flash_attention_forward(
154
+ module: torch.nn.Module,
155
+ query: torch.Tensor,
156
+ key: torch.Tensor,
157
+ value: torch.Tensor,
158
+ attention_mask: Optional[torch.Tensor],
159
+ dropout: float = 0.0,
160
+ scaling: Optional[float] = None,
161
+ sliding_window: Optional[int] = None,
162
+ softcap: Optional[float] = None,
163
+ **kwargs,
164
+ ) -> Tuple[torch.Tensor, None]:
165
+ # This is before the transpose
166
+ q_len = query.shape[2]
167
+
168
+ # FA2 uses non-transposed inputs
169
+ query = query.transpose(1, 2)
170
+ key = key.transpose(1, 2)
171
+ value = value.transpose(1, 2)
172
+
173
+ # FA2 always relies on the value set in the module, so remove it if present in kwargs to avoid passing it twice
174
+ kwargs.pop("is_causal", None)
175
+
176
+ attn_output = _custom_flash_attention_forward(
177
+ query,
178
+ key,
179
+ value,
180
+ attention_mask,
181
+ query_length=q_len,
182
+ is_causal=True,
183
+ dropout=dropout,
184
+ softmax_scale=scaling,
185
+ sliding_window=sliding_window,
186
+ softcap=softcap,
187
+ use_top_left_mask=_flash_use_top_left_mask,
188
+ **kwargs,
189
+ )
190
+
191
+ return attn_output, None
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/models/transformers/qwen2_vl.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team
2
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
3
+ # Based on:
4
+ # https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/models/qwen2_vl/modeling_qwen2_vl.py
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ from typing import Optional, Tuple
19
+
20
+ import torch
21
+
22
+ from .flash_attention_utils import flash_attention_forward
23
+
24
+
25
+ try:
26
+ from transformers.models.qwen2_vl.modeling_qwen2_vl import (
27
+ Qwen2VLAttention,
28
+ apply_multimodal_rotary_pos_emb,
29
+ repeat_kv,
30
+ )
31
+ from transformers.models.qwen2_vl.processing_qwen2_vl import Qwen2VLProcessor
32
+ except ImportError:
33
+ pass
34
+
35
+
36
+ def get_rope_index(
37
+ processor: "Qwen2VLProcessor",
38
+ input_ids: torch.Tensor,
39
+ image_grid_thw: Optional[torch.Tensor] = None,
40
+ video_grid_thw: Optional[torch.Tensor] = None,
41
+ second_per_grid_ts: Optional[torch.Tensor] = None,
42
+ attention_mask: Optional[torch.Tensor] = None,
43
+ ) -> torch.Tensor:
44
+ """
45
+ Gets the position ids for Qwen2-VL, it should be generated before sharding the sequence.
46
+ The batch dim has been removed and the input_ids should be a 1D tensor representing a single example.
47
+ https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1546
48
+ """
49
+ spatial_merge_size = processor.image_processor.merge_size
50
+ tokens_per_second = 2
51
+ image_token_id = processor.tokenizer.convert_tokens_to_ids("<|image_pad|>")
52
+ video_token_id = processor.tokenizer.convert_tokens_to_ids("<|video_pad|>")
53
+ vision_start_token_id = processor.tokenizer.convert_tokens_to_ids("<|vision_start|>")
54
+ if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None):
55
+ if attention_mask is None:
56
+ attention_mask = torch.ones_like(input_ids)
57
+
58
+ position_ids = torch.ones(3, input_ids.size(0), dtype=input_ids.dtype, device=input_ids.device) # (3, seqlen)
59
+ image_index, video_index = 0, 0
60
+ input_ids = input_ids[attention_mask == 1]
61
+ image_nums, video_nums = 0, 0
62
+ vision_start_indices = torch.argwhere(input_ids == vision_start_token_id)
63
+ vision_tokens = input_ids[vision_start_indices + 1]
64
+ image_nums = (vision_tokens == image_token_id).sum()
65
+ video_nums = (vision_tokens == video_token_id).sum()
66
+ input_tokens = input_ids.tolist()
67
+ llm_pos_ids_list: list = []
68
+ st = 0
69
+ remain_images, remain_videos = image_nums, video_nums
70
+ for _ in range(image_nums + video_nums):
71
+ if image_token_id in input_tokens and remain_images > 0:
72
+ ed_image = input_tokens.index(image_token_id, st)
73
+ else:
74
+ ed_image = len(input_tokens) + 1
75
+ if video_token_id in input_tokens and remain_videos > 0:
76
+ ed_video = input_tokens.index(video_token_id, st)
77
+ else:
78
+ ed_video = len(input_tokens) + 1
79
+ if ed_image < ed_video:
80
+ t, h, w = (
81
+ image_grid_thw[image_index][0],
82
+ image_grid_thw[image_index][1],
83
+ image_grid_thw[image_index][2],
84
+ )
85
+ second_per_grid_t = 0
86
+ image_index += 1
87
+ remain_images -= 1
88
+ ed = ed_image
89
+ else:
90
+ t, h, w = (
91
+ video_grid_thw[video_index][0],
92
+ video_grid_thw[video_index][1],
93
+ video_grid_thw[video_index][2],
94
+ )
95
+ if second_per_grid_ts is not None:
96
+ second_per_grid_t = second_per_grid_ts[video_index]
97
+ else:
98
+ second_per_grid_t = 1.0
99
+
100
+ video_index += 1
101
+ remain_videos -= 1
102
+ ed = ed_video
103
+
104
+ llm_grid_t, llm_grid_h, llm_grid_w = (
105
+ t.item(),
106
+ h.item() // spatial_merge_size,
107
+ w.item() // spatial_merge_size,
108
+ )
109
+ text_len = ed - st
110
+
111
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
112
+ llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
113
+
114
+ t_index = torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w)
115
+ t_index = (t_index * second_per_grid_t * tokens_per_second).long().flatten()
116
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten()
117
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten()
118
+ llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx)
119
+ st = ed + llm_grid_t * llm_grid_h * llm_grid_w
120
+
121
+ if st < len(input_tokens):
122
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
123
+ text_len = len(input_tokens) - st
124
+ llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
125
+
126
+ llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
127
+ position_ids[..., attention_mask == 1] = llm_positions.to(position_ids.device)
128
+ else:
129
+ if attention_mask is not None:
130
+ position_ids = attention_mask.long().cumsum(-1) - 1
131
+ position_ids.masked_fill_(attention_mask == 0, 1)
132
+ position_ids = position_ids.unsqueeze(0).expand(3, -1).to(input_ids.device)
133
+ else:
134
+ position_ids = torch.arange(input_ids.shape[1], device=input_ids.device).view(1, -1).expand(3, -1)
135
+
136
+ return position_ids
137
+
138
+
139
+ def qwen2_vl_attn_forward(
140
+ self: "Qwen2VLAttention",
141
+ hidden_states: torch.Tensor,
142
+ attention_mask: Optional[torch.Tensor] = None,
143
+ position_ids: Optional[torch.LongTensor] = None,
144
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # will become mandatory in v4.46
145
+ **kwargs,
146
+ ) -> Tuple[torch.Tensor, None, None]:
147
+ bsz, q_len, _ = hidden_states.size() # q_len = seq_length / sp_size
148
+ query_states = self.q_proj(hidden_states) # (batch_size, seq_length / sp_size, num_heads * head_size)
149
+ key_states = self.k_proj(hidden_states)
150
+ value_states = self.v_proj(hidden_states)
151
+
152
+ query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
153
+ key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
154
+ value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
155
+
156
+ # Because the input can be padded, the absolute sequence length depends on the max position id.
157
+ if position_embeddings is None:
158
+ cos, sin = self.rotary_emb(value_states, position_ids)
159
+ else:
160
+ cos, sin = position_embeddings
161
+
162
+ query_states, key_states = apply_multimodal_rotary_pos_emb(
163
+ query_states, key_states, cos, sin, self.rope_scaling["mrope_section"]
164
+ )
165
+ key_states = repeat_kv(key_states, self.num_key_value_groups)
166
+ value_states = repeat_kv(value_states, self.num_key_value_groups)
167
+ dropout_rate = 0.0 if not self.training else self.attention_dropout
168
+
169
+ sliding_window = None
170
+ if (
171
+ self.config.use_sliding_window
172
+ and getattr(self.config, "sliding_window", None) is not None
173
+ and self.layer_idx >= self.config.max_window_layers
174
+ ):
175
+ sliding_window = self.config.sliding_window
176
+
177
+ attn_output, _ = flash_attention_forward(
178
+ self,
179
+ query_states,
180
+ key_states,
181
+ value_states,
182
+ attention_mask,
183
+ dropout=dropout_rate,
184
+ sliding_window=sliding_window,
185
+ position_ids=position_ids, # important: pass position ids
186
+ ) # (batch_size, seq_length, num_head / sp_size, head_size)
187
+ attn_output = attn_output.reshape(bsz, q_len, self.hidden_size).contiguous()
188
+ attn_output = self.o_proj(attn_output)
189
+ return attn_output, None, None
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/protocol.py ADDED
@@ -0,0 +1,689 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ Implement base data transfer protocol between any two functions, modules.
16
+ We can subclass Protocol to define more detailed batch info with specific keys
17
+ """
18
+
19
+ import copy
20
+ import io
21
+ import pickle
22
+ from collections import defaultdict
23
+ from dataclasses import dataclass, field
24
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
25
+
26
+ import numpy as np
27
+ import ray
28
+ import torch
29
+ from numpy.typing import NDArray
30
+ from tensordict import TensorDict
31
+ from torch.distributed import ProcessGroup
32
+ from torch.utils.data import DataLoader
33
+
34
+ from .utils.py_functional import union_two_dict
35
+
36
+
37
+ try:
38
+ import tensordict
39
+
40
+ tensordict.set_lazy_legacy(False).set()
41
+ except Exception:
42
+ pass
43
+
44
+
45
+ __all__ = ["DataProto", "union_tensor_dict"]
46
+
47
+
48
+ def pad_dataproto_to_divisor(data: "DataProto", size_divisor: int) -> Tuple["DataProto", int]:
49
+ """Pad a DataProto to size divisible by size_divisor
50
+
51
+ Args:
52
+ data (DataProto): the unpadded DataProto
53
+ size_divisor (int): size divisor
54
+
55
+ Returns:
56
+ data (DataProto): the padded DataProto
57
+ pad_size (int)
58
+ """
59
+ assert isinstance(data, DataProto), "data must be a DataProto"
60
+ if len(data) % size_divisor != 0:
61
+ pad_size = size_divisor - len(data) % size_divisor
62
+ padding_protos = []
63
+ remaining_pad = pad_size
64
+ while remaining_pad > 0:
65
+ take_size = min(remaining_pad, len(data))
66
+ padding_protos.append(data[:take_size])
67
+ remaining_pad -= take_size
68
+
69
+ data_padded = DataProto.concat([data] + padding_protos)
70
+ else:
71
+ pad_size = 0
72
+ data_padded = data
73
+
74
+ return data_padded, pad_size
75
+
76
+
77
+ def unpad_dataproto(data: "DataProto", pad_size: int) -> "DataProto":
78
+ if pad_size != 0:
79
+ data = data[:-pad_size]
80
+
81
+ return data
82
+
83
+
84
+ def union_tensor_dict(tensor_dict1: TensorDict, tensor_dict2: TensorDict) -> TensorDict:
85
+ """Union two tensordicts."""
86
+ if tensor_dict1.batch_size != tensor_dict2.batch_size:
87
+ raise ValueError(
88
+ f"Two tensor dict must have identical batch size. Got {tensor_dict1.batch_size} and {tensor_dict2.batch_size}"
89
+ )
90
+
91
+ for key in tensor_dict2.keys():
92
+ if key in tensor_dict1 and not torch.equal(tensor_dict1[key], tensor_dict2[key]):
93
+ raise ValueError(f"Key already exists: {key}.")
94
+
95
+ tensor_dict1[key] = tensor_dict2[key]
96
+
97
+ return tensor_dict1
98
+
99
+
100
+ def union_numpy_dict(tensor_dict1: Dict[str, NDArray], tensor_dict2: Dict[str, NDArray]) -> Dict[str, NDArray]:
101
+ for key in tensor_dict2.keys():
102
+ if key in tensor_dict1:
103
+ assert isinstance(tensor_dict2[key], np.ndarray)
104
+ assert isinstance(tensor_dict1[key], np.ndarray)
105
+ if not np.all(tensor_dict1[key] == tensor_dict2[key]):
106
+ raise ValueError(f"Key already exists: {key}.")
107
+
108
+ tensor_dict1[key] = tensor_dict2[key]
109
+
110
+ return tensor_dict1
111
+
112
+
113
+ def batch_collate(features: List[Dict[str, Any]]) -> Dict[str, List[Any]]:
114
+ if len(features) == 0:
115
+ return {}
116
+
117
+ batch_features = defaultdict(list)
118
+ for feature in features:
119
+ for key, value in feature.items():
120
+ batch_features[key].append(value)
121
+
122
+ return batch_features
123
+
124
+
125
+ def fold_batch_dim(data: "DataProto", new_batch_size: int):
126
+ """
127
+ Fold a batch dim from [bsz, xxx] into [new_bsz, bsz // new_bsz, xxx]
128
+ """
129
+ batch_size = data.batch.batch_size[0]
130
+
131
+ assert batch_size % new_batch_size == 0
132
+
133
+ tensor: TensorDict = data.batch
134
+ non_tensor = data.non_tensor_batch
135
+
136
+ tensor = tensor.view(new_batch_size, -1)
137
+ tensor.auto_batch_size_(batch_dims=1)
138
+
139
+ for key, value in non_tensor.items():
140
+ non_tensor[key] = np.reshape(value, newshape=(new_batch_size, -1, *value.shape[1:]))
141
+
142
+ return DataProto(batch=tensor, non_tensor_batch=non_tensor, meta_info=data.meta_info)
143
+
144
+
145
+ def collate_fn(data_items: list["DataProtoItem"]):
146
+ batch = []
147
+ non_tensor_batch = []
148
+ for data in data_items:
149
+ batch.append(data.batch)
150
+ non_tensor_batch.append(data.non_tensor_batch)
151
+
152
+ batch = torch.stack(batch).contiguous()
153
+ non_tensor_batch = batch_collate(non_tensor_batch)
154
+ non_tensor_batch = {key: np.array(value, dtype=object) for key, value in non_tensor_batch.items()}
155
+ return DataProto(batch=batch, non_tensor_batch=non_tensor_batch)
156
+
157
+
158
+ @dataclass
159
+ class DataProtoItem:
160
+ batch: Optional[TensorDict] = None
161
+ non_tensor_batch: Dict[str, NDArray] = field(default_factory=dict)
162
+ meta_info: Dict[str, Any] = field(default_factory=dict)
163
+
164
+
165
+ @dataclass
166
+ class DataProto:
167
+ """
168
+ A DataProto is a data structure that aims to provide a standard protocol for data exchange between functions.
169
+ It contains a batch (TensorDict) and a meta_info (Dict). The batch is a TensorDict https://pytorch.org/tensordict/.
170
+ TensorDict allows you to manipulate a dictionary of Tensors like a single Tensor. Ideally, the tensors with the
171
+ same batch size should be put inside batch.
172
+ """
173
+
174
+ batch: Optional[TensorDict] = None
175
+ non_tensor_batch: Dict[str, NDArray] = field(default_factory=dict)
176
+ meta_info: Dict[str, Any] = field(default_factory=dict)
177
+
178
+ def __post_init__(self):
179
+ self.check_consistency() # perform necessary checking
180
+
181
+ def __len__(self) -> int:
182
+ if self.batch is not None:
183
+ return self.batch.batch_size[0]
184
+ elif self.non_tensor_batch is not None and len(self.non_tensor_batch) > 0:
185
+ pivot_key = list(self.non_tensor_batch.keys())[0]
186
+ return self.non_tensor_batch[pivot_key].shape[0]
187
+ else:
188
+ return 0
189
+
190
+ def __getitem__(self, item: Union[int, slice]) -> Union["DataProto", "DataProtoItem"]:
191
+ tensor_data = self.batch[item]
192
+ non_tensor_data = {key: value[item] for key, value in self.non_tensor_batch.items()}
193
+ return_type = DataProto if isinstance(item, slice) else DataProtoItem
194
+ return return_type(batch=tensor_data, non_tensor_batch=non_tensor_data, meta_info=self.meta_info)
195
+
196
+ def __getstate__(self) -> Tuple[bytes, Dict[str, NDArray], Dict[str, Any]]:
197
+ buffer = io.BytesIO()
198
+ if self.batch is not None:
199
+ self.batch: TensorDict = self.batch.contiguous()
200
+ self.batch: TensorDict = self.batch.consolidate()
201
+
202
+ torch.save(self.batch, buffer)
203
+ buffer_bytes = buffer.getvalue()
204
+ return buffer_bytes, self.non_tensor_batch, self.meta_info
205
+
206
+ def __setstate__(self, data: Tuple[bytes, Dict[str, NDArray], Dict[str, Any]]) -> None:
207
+ batch_deserialized_bytes, non_tensor_batch, meta_info = data
208
+ batch_deserialized = io.BytesIO(batch_deserialized_bytes)
209
+ batch = torch.load(batch_deserialized, weights_only=False, map_location="cpu")
210
+ self.batch = batch
211
+ self.non_tensor_batch = non_tensor_batch
212
+ self.meta_info = meta_info
213
+
214
+ def save_to_disk(self, filepath: str) -> None:
215
+ with open(filepath, "wb") as f:
216
+ pickle.dump(self, f)
217
+
218
+ @staticmethod
219
+ def load_from_disk(filepath: str) -> "DataProto":
220
+ with open(filepath, "rb") as f:
221
+ data = pickle.load(f)
222
+ return data
223
+
224
+ def print_size(self, prefix: str = "") -> None:
225
+ size_of_tensordict = 0
226
+ if self.batch is not None:
227
+ for tensor in self.batch.values():
228
+ if isinstance(tensor, torch.Tensor):
229
+ size_of_tensordict += tensor.element_size() * tensor.numel()
230
+
231
+ size_of_numpy_array = 0
232
+ for value in self.non_tensor_batch.values():
233
+ size_of_numpy_array += value.nbytes
234
+
235
+ size_of_numpy_array /= 1024**3
236
+ size_of_tensordict /= 1024**3
237
+
238
+ message = f"Size of tensordict: {size_of_tensordict} GB, size of non_tensor_batch: {size_of_numpy_array} GB."
239
+ print({prefix}, {message})
240
+
241
+ def check_consistency(self):
242
+ """Check the consistency of the DataProto. Mainly for batch and non_tensor_batch
243
+ We expose this function as a public one so that user can call themselves directly
244
+ """
245
+ if self.batch is not None:
246
+ assert len(self.batch.batch_size) == 1, "only support num_batch_dims=1"
247
+
248
+ if self.batch is not None and len(self.non_tensor_batch) != 0:
249
+ # TODO: we can actually lift this restriction if needed
250
+ assert len(self.batch.batch_size) == 1, "only support num_batch_dims=1 when non_tensor_batch is not empty."
251
+
252
+ batch_size = self.batch.batch_size[0]
253
+ for key, value in self.non_tensor_batch.items():
254
+ assert len(value) == batch_size, f"key {key} length {len(value)} is not equal to bsz {batch_size}."
255
+
256
+ @classmethod
257
+ def from_single_dict(
258
+ cls,
259
+ data: Dict[str, Union[torch.Tensor, NDArray]],
260
+ meta_info: Optional[Dict[str, Any]] = None,
261
+ ) -> "DataProto":
262
+ tensors, non_tensors = {}, {}
263
+ for key, value in data.items():
264
+ if isinstance(value, torch.Tensor):
265
+ tensors[key] = value
266
+ elif isinstance(value, np.ndarray):
267
+ non_tensors[key] = value
268
+ else:
269
+ raise ValueError(f"Unsupported type in data {type(value)}")
270
+
271
+ return DataProto.from_dict(tensors=tensors, non_tensors=non_tensors, meta_info=meta_info)
272
+
273
+ @classmethod
274
+ def from_dict(
275
+ cls,
276
+ tensors: Dict[str, torch.Tensor],
277
+ non_tensors: Dict[str, NDArray] = None,
278
+ meta_info: Optional[Dict[str, Any]] = None,
279
+ num_batch_dims: int = 1,
280
+ ) -> "DataProto":
281
+ """Create a DataProto from a dict of tensors. This assumes that
282
+ 1. All the tensor in tensors have the same dim0
283
+ 2. Only dim0 is the batch dim
284
+ """
285
+ assert len(tensors) > 0, "tensors must not be empty"
286
+ assert num_batch_dims > 0, "num_batch_dims must be greater than zero"
287
+ if non_tensors is not None:
288
+ assert num_batch_dims == 1, "only support num_batch_dims=1 when non_tensors is not None."
289
+
290
+ meta_info = meta_info or {}
291
+ non_tensors = non_tensors or {}
292
+ assert isinstance(non_tensors, dict), "non_tensors should be a dictionary."
293
+
294
+ # get and check batch size
295
+ batch_size = None
296
+ pivot_key = None
297
+ for key, tensor in tensors.items():
298
+ if batch_size is None:
299
+ batch_size = tensor.shape[:num_batch_dims]
300
+ pivot_key = key
301
+ else:
302
+ current_batch = tensor.shape[:num_batch_dims]
303
+ assert batch_size == current_batch, (
304
+ f"Not all the tensor in tensors have the same batch size with batch_dims={num_batch_dims}. "
305
+ f"Got {pivot_key} has {batch_size}, {key} has {current_batch}"
306
+ )
307
+
308
+ tensor_dict = TensorDict(source=tensors, batch_size=batch_size)
309
+ return cls(batch=tensor_dict, non_tensor_batch=non_tensors, meta_info=meta_info)
310
+
311
+ def to(self, device: torch.device) -> "DataProto":
312
+ """move the batch to device
313
+
314
+ Args:
315
+ device (torch.device, str): torch device
316
+
317
+ Returns:
318
+ DataProto: the current DataProto
319
+
320
+ """
321
+ if self.batch is not None:
322
+ self.batch = self.batch.to(device)
323
+
324
+ return self
325
+
326
+ def select(
327
+ self,
328
+ batch_keys: Optional[List[str]] = None,
329
+ non_tensor_batch_keys: Optional[List[str]] = None,
330
+ meta_info_keys: Optional[List[str]] = None,
331
+ deepcopy: bool = False,
332
+ ) -> "DataProto":
333
+ """Select a subset of the DataProto via batch_keys and meta_info_keys
334
+
335
+ Args:
336
+ batch_keys (list, optional): a list of strings indicating the keys in batch to select
337
+ meta_info_keys (list, optional): a list of keys indicating the meta info to select
338
+
339
+ Returns:
340
+ DataProto: the DataProto with the selected batch_keys and meta_info_keys
341
+ """
342
+ # TODO (zhangchi.usc1992) whether to copy
343
+ if batch_keys is not None:
344
+ batch_keys = tuple(batch_keys)
345
+ sub_batch = self.batch.select(*batch_keys)
346
+ else:
347
+ sub_batch = self.batch
348
+
349
+ if non_tensor_batch_keys is not None:
350
+ non_tensor_batch = {k: v for k, v in self.non_tensor_batch.items() if k in non_tensor_batch_keys}
351
+ else:
352
+ non_tensor_batch = self.non_tensor_batch
353
+
354
+ if deepcopy:
355
+ non_tensor_batch = copy.deepcopy(non_tensor_batch)
356
+
357
+ if meta_info_keys is not None:
358
+ sub_meta_info = {k: v for k, v in self.meta_info.items() if k in meta_info_keys}
359
+ else:
360
+ sub_meta_info = self.meta_info
361
+
362
+ if deepcopy:
363
+ sub_meta_info = copy.deepcopy(sub_meta_info)
364
+
365
+ return DataProto(batch=sub_batch, non_tensor_batch=non_tensor_batch, meta_info=sub_meta_info)
366
+
367
+ def pop(
368
+ self,
369
+ batch_keys: Optional[List[str]] = None,
370
+ non_tensor_batch_keys: Optional[List[str]] = None,
371
+ meta_info_keys: Optional[List[str]] = None,
372
+ ) -> "DataProto":
373
+ """Pop a subset of the DataProto via `batch_keys` and `meta_info_keys`
374
+
375
+ Args:
376
+ batch_keys (list, optional): a list of strings indicating the keys in batch to pop
377
+ meta_info_keys (list, optional): a list of keys indicating the meta info to pop
378
+
379
+ Returns:
380
+ DataProto: the DataProto with the poped batch_keys and meta_info_keys
381
+ """
382
+ assert batch_keys is not None
383
+ non_tensor_batch_keys = non_tensor_batch_keys or []
384
+ meta_info_keys = meta_info_keys or []
385
+
386
+ tensors = {}
387
+ for key in batch_keys:
388
+ tensors[key] = self.batch.pop(key)
389
+
390
+ non_tensors = {}
391
+ for key in non_tensor_batch_keys:
392
+ non_tensors[key] = self.non_tensor_batch.pop(key)
393
+
394
+ meta_info = {}
395
+ for key in meta_info_keys:
396
+ meta_info[key] = self.meta_info.pop(key)
397
+
398
+ return DataProto.from_dict(tensors=tensors, non_tensors=non_tensors, meta_info=meta_info)
399
+
400
+ def rename(
401
+ self, old_keys: Optional[Union[str, List[str]]] = None, new_keys: Optional[Union[str, List[str]]] = None
402
+ ) -> "DataProto":
403
+ """
404
+ Note that this function only rename the key in the batch
405
+ """
406
+
407
+ def validate_input(keys):
408
+ if keys is not None:
409
+ if isinstance(keys, str):
410
+ keys = [keys]
411
+ elif isinstance(keys, list):
412
+ pass
413
+ else:
414
+ raise TypeError(f"keys must be a list or a string, but got {type(keys)}")
415
+ return keys
416
+
417
+ old_keys = validate_input(old_keys)
418
+ new_keys = validate_input(new_keys)
419
+
420
+ if len(new_keys) != len(old_keys):
421
+ raise ValueError(
422
+ f"new_keys and old_keys must have the same length, but got {len(new_keys)} and {len(old_keys)}"
423
+ )
424
+
425
+ self.batch.rename_key_(tuple(old_keys), tuple(new_keys))
426
+
427
+ return self
428
+
429
+ def union(self, other: "DataProto") -> "DataProto":
430
+ """Union with another DataProto. Union batch and meta_info separately.
431
+ Throw an error if
432
+ - there are conflict keys in batch and they are not equal
433
+ - the batch size of two data batch is not the same
434
+ - there are conflict keys in meta_info and they are not the same.
435
+
436
+ Args:
437
+ other (DataProto): another DataProto to union
438
+
439
+ Returns:
440
+ DataProto: the DataProto after union
441
+ """
442
+ self.batch = union_tensor_dict(self.batch, other.batch)
443
+ self.non_tensor_batch = union_numpy_dict(self.non_tensor_batch, other.non_tensor_batch)
444
+ self.meta_info = union_two_dict(self.meta_info, other.meta_info)
445
+ return self
446
+
447
+ def make_iterator(
448
+ self, mini_batch_size: int, epochs: int, seed: int = None, dataloader_kwargs: Dict[str, Any] = None
449
+ ):
450
+ """Make an iterator from the DataProto. This is built upon that TensorDict can be used as a normal Pytorch
451
+ dataset. See https://pytorch.org/tensordict/tutorials/data_fashion for more details.
452
+
453
+ Args:
454
+ mini_batch_size (int): mini-batch size when iterating the dataset. We require that
455
+ ``batch.batch_size[0] % mini_batch_size == 0``
456
+ epochs (int): number of epochs when iterating the dataset.
457
+ dataloader_kwargs: internally, it returns a DataLoader over the batch.
458
+ The dataloader_kwargs is the kwargs passed to the DataLoader
459
+
460
+ Returns:
461
+ Iterator: an iterator that yields a mini-batch data at a time. The total number of iteration steps is
462
+ ``self.batch.batch_size * epochs // mini_batch_size``
463
+ """
464
+ assert self.batch.batch_size[0] % mini_batch_size == 0, f"{self.batch.batch_size[0]} % {mini_batch_size} != 0"
465
+ # we can directly create a dataloader from TensorDict
466
+ if dataloader_kwargs is None:
467
+ dataloader_kwargs = {}
468
+
469
+ if seed is not None:
470
+ generator = torch.Generator()
471
+ generator.manual_seed(seed)
472
+ else:
473
+ generator = None
474
+
475
+ assert isinstance(dataloader_kwargs, Dict)
476
+ train_dataloader = DataLoader(
477
+ dataset=self, batch_size=mini_batch_size, collate_fn=collate_fn, generator=generator, **dataloader_kwargs
478
+ )
479
+
480
+ def get_data():
481
+ for _ in range(epochs):
482
+ for d in train_dataloader:
483
+ d.meta_info = self.meta_info
484
+ yield d
485
+
486
+ return iter(get_data())
487
+
488
+ def chunk(self, chunks: int) -> List["DataProto"]:
489
+ """Split the batch among dim=0 into chunks. The meta_info is passed to each DataProto after split.
490
+
491
+ Args:
492
+ chunks (int): the number of chunks to split on dim=0
493
+
494
+ Returns:
495
+ List[DataProto]: a list of DataProto after splitting
496
+ """
497
+ assert len(self) % chunks == 0, (
498
+ f"only support equal chunk. Got size of DataProto {len(self)} and chunk {chunks}."
499
+ )
500
+ if self.batch is not None:
501
+ batch_lst = self.batch.chunk(chunks=chunks, dim=0)
502
+ else:
503
+ batch_lst = [None for _ in range(chunks)]
504
+
505
+ non_tensor_batch_lst = [{} for _ in range(chunks)]
506
+ for key, value in self.non_tensor_batch.items():
507
+ assert isinstance(value, np.ndarray)
508
+ non_tensor_lst = np.array_split(value, chunks)
509
+ assert len(non_tensor_lst) == chunks
510
+ for i in range(chunks):
511
+ non_tensor_batch_lst[i][key] = non_tensor_lst[i]
512
+
513
+ output = []
514
+ for i in range(chunks):
515
+ output.append(
516
+ DataProto(batch=batch_lst[i], non_tensor_batch=non_tensor_batch_lst[i], meta_info=self.meta_info)
517
+ )
518
+
519
+ return output
520
+
521
+ def split(self, split_size: int) -> List["DataProto"]:
522
+ chunks = len(self) // split_size
523
+ return self.chunk(chunks)
524
+
525
+ @staticmethod
526
+ def concat(data: List["DataProto"]) -> "DataProto":
527
+ """Concat a list of DataProto. The batch is concatenated among dim=0.
528
+ The meta_info is assumed to be identical and will use the first one.
529
+
530
+ Args:
531
+ data (List[DataProto]): list of DataProto
532
+
533
+ Returns:
534
+ DataProto: concatenated DataProto
535
+ """
536
+ batch_lst = [batch.batch for batch in data]
537
+ if batch_lst[0] is not None:
538
+ new_batch = torch.cat(batch_lst, dim=0)
539
+ else:
540
+ new_batch = None
541
+
542
+ non_tensor_batch = batch_collate([d.non_tensor_batch for d in data])
543
+ for key, value in non_tensor_batch.items():
544
+ non_tensor_batch[key] = np.concatenate(value, axis=0)
545
+
546
+ return DataProto(batch=new_batch, non_tensor_batch=non_tensor_batch, meta_info=data[0].meta_info)
547
+
548
+ def reorder(self, indices: torch.Tensor) -> None:
549
+ """
550
+ Note that this operation is in-place
551
+ """
552
+ indices_np = indices.detach().numpy()
553
+ self.batch = self.batch[indices]
554
+ self.non_tensor_batch = {key: value[indices_np] for key, value in self.non_tensor_batch.items()}
555
+
556
+ def repeat(self, repeat_times: int = 2, interleave: bool = True) -> "DataProto":
557
+ """
558
+ Repeat the batch data a specified number of times.
559
+
560
+ Args:
561
+ repeat_times (int): Number of times to repeat the data.
562
+ interleave (bool): Whether to interleave the repeated data.
563
+
564
+ Returns:
565
+ DataProto: A new DataProto with repeated data.
566
+ """
567
+ if self.batch is not None:
568
+ if interleave:
569
+ # Interleave the data
570
+ repeated_tensors = {
571
+ key: tensor.repeat_interleave(repeat_times, dim=0) for key, tensor in self.batch.items()
572
+ }
573
+ else:
574
+ # Stack the data
575
+ repeated_tensors = {
576
+ key: tensor.unsqueeze(0).expand(repeat_times, *tensor.shape).reshape(-1, *tensor.shape[1:])
577
+ for key, tensor in self.batch.items()
578
+ }
579
+
580
+ repeated_batch = TensorDict(
581
+ source=repeated_tensors,
582
+ batch_size=(self.batch.batch_size[0] * repeat_times,),
583
+ )
584
+ else:
585
+ repeated_batch = None
586
+
587
+ repeated_non_tensor_batch = {}
588
+ for key, value in self.non_tensor_batch.items():
589
+ if interleave:
590
+ repeated_non_tensor_batch[key] = np.repeat(value, repeat_times, axis=0)
591
+ else:
592
+ repeated_non_tensor_batch[key] = np.tile(value, (repeat_times,) + (1,) * (value.ndim - 1))
593
+
594
+ return DataProto(
595
+ batch=repeated_batch,
596
+ non_tensor_batch=repeated_non_tensor_batch,
597
+ meta_info=self.meta_info,
598
+ )
599
+
600
+
601
+ @dataclass
602
+ class DataProtoFuture:
603
+ """
604
+ DataProtoFuture aims to eliminate actual data fetching on driver. By doing so, the driver doesn't have to wait
605
+ for data so that asynchronous execution becomes possible.
606
+ DataProtoFuture contains a list of futures from another WorkerGroup of size world_size.
607
+ - collect_fn is a Callable that reduces the list of futures to a DataProto
608
+ - dispatch_fn is a Callable that partitions the DataProto into a list of DataProto of size world_size and then select
609
+
610
+ Potential issue: we can optimize dispatch_fn(collect_fn) such that only needed data is fetched on destination
611
+ - DataProtoFuture only supports directly passing from the output of a method to another input. You can't perform any
612
+ operation on the DataProtoFuture in driver.
613
+ """
614
+
615
+ collect_fn: Callable
616
+ futures: List[ray.ObjectRef]
617
+ dispatch_fn: Callable = None
618
+
619
+ @staticmethod
620
+ def concat(data: List[ray.ObjectRef]) -> "DataProtoFuture":
621
+ output = DataProtoFuture(collect_fn=DataProto.concat, futures=data)
622
+ return output
623
+
624
+ def chunk(self, chunks: int) -> List["DataProtoFuture"]:
625
+ from functools import partial
626
+
627
+ arg_future_lst = []
628
+ for i in range(chunks):
629
+ # note that we can't directly pass i and chunks
630
+ def dispatch_fn(x, i, chunks):
631
+ return x.chunk(chunks=chunks)[i]
632
+
633
+ arg_future = DataProtoFuture(
634
+ collect_fn=self.collect_fn, dispatch_fn=partial(dispatch_fn, i=i, chunks=chunks), futures=self.futures
635
+ )
636
+ arg_future_lst.append(arg_future)
637
+ return arg_future_lst
638
+
639
+ def get(self):
640
+ outputs = ray.get(self.futures) # dp_size.
641
+ for output in outputs:
642
+ assert isinstance(output, DataProto)
643
+
644
+ outputs = self.collect_fn(outputs) # select dp, concat
645
+ if self.dispatch_fn is not None:
646
+ outputs = self.dispatch_fn(outputs) # split in batch dim, select using dp
647
+
648
+ return outputs
649
+
650
+
651
+ def allgather_dict_tensors(
652
+ tensors: Union[Dict[str, torch.Tensor], TensorDict], size: int, group: ProcessGroup, dim: int = 0
653
+ ) -> Union[Dict[str, torch.Tensor], TensorDict]:
654
+ """
655
+ TODO: optimize this.
656
+ - We can use async ops
657
+ - We can use only one allgather
658
+ """
659
+ if isinstance(tensors, TensorDict):
660
+ is_tensor_dict = True
661
+ tensors_as_dict = tensors.to_dict()
662
+ else:
663
+ tensors_as_dict = tensors
664
+ is_tensor_dict = False
665
+
666
+ output = {}
667
+ sorted_keys = sorted(tensors_as_dict.keys())
668
+ for key in sorted_keys:
669
+ value = tensors_as_dict[key]
670
+ output[key] = [torch.empty_like(value) for _ in range(size)]
671
+ torch.distributed.all_gather(output[key], value, group=group, async_op=False)
672
+ output[key] = torch.cat(output[key], dim=dim)
673
+
674
+ if is_tensor_dict:
675
+ output = TensorDict(source=output, batch_size=tensors.batch_size[0] * size)
676
+
677
+ return output
678
+
679
+
680
+ def all_gather_data_proto(data: DataProto, size: int, group: ProcessGroup) -> None:
681
+ # Note that this is an inplace operator just like torch.distributed.all_gather
682
+ prev_device = data.batch.device
683
+ data.batch = data.batch.cuda(device=torch.cuda.current_device())
684
+ data.batch = allgather_dict_tensors(data.batch.contiguous(), size=size, group=group, dim=0)
685
+ data.batch = data.batch.to(prev_device)
686
+ # all gather non_tensor_batch
687
+ all_non_tensor_batch = [None for _ in range(size)]
688
+ torch.distributed.all_gather_object(all_non_tensor_batch, data.non_tensor_batch, group=group)
689
+ data.non_tensor_batch = {k: np.concatenate([d[k] for d in all_non_tensor_batch]) for k in data.non_tensor_batch}
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/pyproject.toml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "verl"
7
+ dynamic = [
8
+ "version",
9
+ "dependencies",
10
+ "optional-dependencies",
11
+ "requires-python",
12
+ "authors",
13
+ "description",
14
+ "readme",
15
+ "license"
16
+ ]
17
+
18
+ [tool.ruff]
19
+ target-version = "py39"
20
+ line-length = 119
21
+ indent-width = 4
22
+
23
+ [tool.ruff.lint]
24
+ ignore = ["C901", "E501", "E741", "W605", "C408"]
25
+ select = ["C", "E", "F", "I", "W", "RUF022"]
26
+
27
+ [tool.ruff.lint.per-file-ignores]
28
+ "__init__.py" = ["E402", "F401", "F403", "F811"]
29
+
30
+ [tool.ruff.lint.isort]
31
+ lines-after-imports = 2
32
+ known-first-party = ["verl"]
33
+ known-third-party = ["torch", "transformers", "wandb"]
34
+
35
+ [tool.ruff.format]
36
+ quote-style = "double"
37
+ indent-style = "space"
38
+ skip-magic-trailing-comma = false
39
+ line-ending = "auto"
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate
2
+ codetiming
3
+ datasets
4
+ liger-kernel
5
+ mathruler
6
+ numpy
7
+ omegaconf
8
+ pandas
9
+ peft
10
+ pillow
11
+ pyarrow>=15.0.0
12
+ pylatexenc
13
+ qwen-vl-utils
14
+ ray[default]
15
+ tensordict
16
+ torchdata
17
+ transformers>=4.51.0
18
+ vllm>=0.7.3
19
+ wandb
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .worker import Worker
16
+ from .worker_group import ClassWithInitArgs, ResourcePool, WorkerGroup
17
+
18
+
19
+ __all__ = ["ClassWithInitArgs", "ResourcePool", "Worker", "WorkerGroup"]
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/decorator.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from enum import Enum, auto
16
+ from functools import wraps
17
+ from types import FunctionType
18
+ from typing import TYPE_CHECKING, Dict, List, Literal, Union
19
+
20
+ import ray
21
+
22
+ from ...protocol import DataProto, DataProtoFuture
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from .worker_group import WorkerGroup
27
+
28
+
29
+ # here we add a magic number of avoid user-defined function already have this attribute
30
+ MAGIC_ATTR = "attrs_3141562937"
31
+
32
+
33
+ class Dispatch(Enum):
34
+ RANK_ZERO = auto()
35
+ ONE_TO_ALL = auto()
36
+ ALL_TO_ALL = auto()
37
+ DP_COMPUTE = auto()
38
+ DP_COMPUTE_PROTO = auto()
39
+ DP_COMPUTE_PROTO_WITH_FUNC = auto()
40
+ DP_COMPUTE_METRIC = auto()
41
+
42
+
43
+ class Execute(Enum):
44
+ ALL = 0
45
+ RANK_ZERO = 1
46
+
47
+
48
+ def _split_args_kwargs_data_proto(chunks: int, *args, **kwargs):
49
+ splitted_args = []
50
+ for arg in args:
51
+ assert isinstance(arg, (DataProto, DataProtoFuture))
52
+ splitted_args.append(arg.chunk(chunks=chunks))
53
+
54
+ splitted_kwargs = {}
55
+ for key, value in kwargs.items():
56
+ assert isinstance(value, (DataProto, DataProtoFuture))
57
+ splitted_kwargs[key] = value.chunk(chunks=chunks)
58
+
59
+ return splitted_args, splitted_kwargs
60
+
61
+
62
+ def dispatch_one_to_all(worker_group: "WorkerGroup", *args, **kwargs):
63
+ args = tuple([arg] * worker_group.world_size for arg in args)
64
+ kwargs = {k: [v] * worker_group.world_size for k, v in kwargs.items()}
65
+ return args, kwargs
66
+
67
+
68
+ def dispatch_all_to_all(worker_group: "WorkerGroup", *args, **kwargs):
69
+ return args, kwargs
70
+
71
+
72
+ def collect_all_to_all(worker_group: "WorkerGroup", output):
73
+ return output
74
+
75
+
76
+ def _concat_data_proto_or_future(outputs: List[DataProto]) -> DataProto:
77
+ # make sure all the elements in output has the same type
78
+ for output in outputs:
79
+ assert type(output) is type(outputs[0])
80
+
81
+ output = outputs[0]
82
+
83
+ if isinstance(output, DataProto):
84
+ return DataProto.concat(outputs)
85
+ elif isinstance(output, ray.ObjectRef):
86
+ return DataProtoFuture.concat(outputs)
87
+ else:
88
+ raise NotImplementedError
89
+
90
+
91
+ def dispatch_dp_compute(worker_group: "WorkerGroup", *args, **kwargs):
92
+ for arg in args:
93
+ assert isinstance(arg, (tuple, list)) and len(arg) == worker_group.world_size
94
+
95
+ for value in kwargs.values():
96
+ assert isinstance(value, (tuple, list)) and len(value) == worker_group.world_size
97
+
98
+ return args, kwargs
99
+
100
+
101
+ def collect_dp_compute(worker_group: "WorkerGroup", outputs: List[DataProto]) -> List[DataProto]:
102
+ assert len(outputs) == worker_group.world_size
103
+ return outputs
104
+
105
+
106
+ def dispatch_dp_compute_data_proto(worker_group: "WorkerGroup", *args, **kwargs):
107
+ splitted_args, splitted_kwargs = _split_args_kwargs_data_proto(worker_group.world_size, *args, **kwargs)
108
+ return splitted_args, splitted_kwargs
109
+
110
+
111
+ def dispatch_dp_compute_data_proto_with_func(worker_group: "WorkerGroup", *args, **kwargs):
112
+ assert type(args[0]) is FunctionType # NOTE: The first one args is a function!
113
+ splitted_args, splitted_kwargs = _split_args_kwargs_data_proto(worker_group.world_size, *args[1:], **kwargs)
114
+ splitted_args_with_func = [[args[0]] * worker_group.world_size] + splitted_args
115
+ return splitted_args_with_func, splitted_kwargs
116
+
117
+
118
+ def collect_dp_compute_data_proto(worker_group: "WorkerGroup", outputs: List[DataProto]) -> DataProto:
119
+ for output in outputs:
120
+ assert isinstance(output, (DataProto, ray.ObjectRef)), f"Expect a DataProto, but got {type(output)}"
121
+
122
+ outputs = collect_dp_compute(worker_group, outputs)
123
+ return _concat_data_proto_or_future(outputs)
124
+
125
+
126
+ def get_predefined_dispatch_fn(dispatch_mode: Dispatch):
127
+ predefined_dispatch_mode_fn = {
128
+ Dispatch.ONE_TO_ALL: {
129
+ "dispatch_fn": dispatch_one_to_all,
130
+ "collect_fn": collect_all_to_all,
131
+ },
132
+ Dispatch.ALL_TO_ALL: {
133
+ "dispatch_fn": dispatch_all_to_all,
134
+ "collect_fn": collect_all_to_all,
135
+ },
136
+ Dispatch.DP_COMPUTE: {
137
+ "dispatch_fn": dispatch_dp_compute,
138
+ "collect_fn": collect_dp_compute,
139
+ },
140
+ Dispatch.DP_COMPUTE_PROTO: {
141
+ "dispatch_fn": dispatch_dp_compute_data_proto,
142
+ "collect_fn": collect_dp_compute_data_proto,
143
+ },
144
+ Dispatch.DP_COMPUTE_PROTO_WITH_FUNC: {
145
+ "dispatch_fn": dispatch_dp_compute_data_proto_with_func,
146
+ "collect_fn": collect_dp_compute_data_proto,
147
+ },
148
+ Dispatch.DP_COMPUTE_METRIC: {
149
+ "dispatch_fn": dispatch_dp_compute_data_proto,
150
+ "collect_fn": collect_dp_compute,
151
+ },
152
+ }
153
+ return predefined_dispatch_mode_fn[dispatch_mode]
154
+
155
+
156
+ def get_predefined_execute_fn(execute_mode: Execute):
157
+ """
158
+ Note that here we only asks execute_all and execute_rank_zero to be implemented
159
+ Leave the choice of how these two functions handle argument 'blocking' to users
160
+ """
161
+ predefined_execute_mode_fn = {
162
+ Execute.ALL: {"execute_fn_name": "execute_all"},
163
+ Execute.RANK_ZERO: {"execute_fn_name": "execute_rank_zero"},
164
+ }
165
+ return predefined_execute_mode_fn[execute_mode]
166
+
167
+
168
+ def _check_dispatch_mode(dispatch_mode: Union[Dispatch, Dict[Literal["dispatch_fn", "collect_fn"], FunctionType]]):
169
+ assert isinstance(dispatch_mode, (Dispatch, dict)), (
170
+ f"dispatch_mode must be a Dispatch or a Dict. Got {dispatch_mode}"
171
+ )
172
+ if isinstance(dispatch_mode, dict):
173
+ necessary_keys = ["dispatch_fn", "collect_fn"]
174
+ for key in necessary_keys:
175
+ assert key in dispatch_mode, f"key {key} should be in dispatch_mode if it is a dictionary"
176
+
177
+
178
+ def _check_execute_mode(execute_mode: Execute):
179
+ assert isinstance(execute_mode, Execute), f"execute_mode must be a Execute. Got {execute_mode}"
180
+
181
+
182
+ def _materialize_futures(*args, **kwargs):
183
+ new_args = []
184
+ for arg in args:
185
+ if isinstance(arg, DataProtoFuture):
186
+ arg = arg.get()
187
+ # add more type to materialize
188
+ new_args.append(arg)
189
+
190
+ for key, value in kwargs.items():
191
+ if isinstance(value, DataProtoFuture):
192
+ kwargs[key] = value.get()
193
+
194
+ new_args = tuple(new_args)
195
+ return new_args, kwargs
196
+
197
+
198
+ def register(dispatch_mode=Dispatch.ALL_TO_ALL, execute_mode=Execute.ALL, blocking=True, materialize_futures=True):
199
+ _check_dispatch_mode(dispatch_mode=dispatch_mode)
200
+ _check_execute_mode(execute_mode=execute_mode)
201
+
202
+ def decorator(func):
203
+ @wraps(func)
204
+ def inner(*args, **kwargs):
205
+ if materialize_futures:
206
+ args, kwargs = _materialize_futures(*args, **kwargs)
207
+ return func(*args, **kwargs)
208
+
209
+ attrs = {"dispatch_mode": dispatch_mode, "execute_mode": execute_mode, "blocking": blocking}
210
+ setattr(inner, MAGIC_ATTR, attrs)
211
+ return inner
212
+
213
+ return decorator
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/register_center/ray.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import ray
16
+
17
+
18
+ @ray.remote
19
+ class WorkerGroupRegisterCenter:
20
+ def __init__(self, rank_zero_info):
21
+ self.rank_zero_info = rank_zero_info
22
+
23
+ def get_rank_zero_info(self):
24
+ return self.rank_zero_info
25
+
26
+
27
+ def create_worker_group_register_center(name, info):
28
+ return WorkerGroupRegisterCenter.options(name=name).remote(info)
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ the class for Worker
16
+ """
17
+
18
+ import os
19
+ import socket
20
+ from dataclasses import dataclass
21
+ from typing import Tuple
22
+
23
+ import ray
24
+ import torch
25
+
26
+ from .decorator import Dispatch, Execute, register
27
+ from .register_center.ray import create_worker_group_register_center
28
+
29
+
30
+ @dataclass
31
+ class DistRankInfo:
32
+ tp_rank: int
33
+ dp_rank: int
34
+ pp_rank: int
35
+
36
+
37
+ @dataclass
38
+ class DistGlobalInfo:
39
+ tp_size: int
40
+ dp_size: int
41
+ pp_size: int
42
+
43
+
44
+ class WorkerHelper:
45
+ def _get_node_ip(self) -> str:
46
+ host_ipv4 = os.getenv("MY_HOST_IP", None)
47
+ host_ipv6 = os.getenv("MY_HOST_IPV6", None)
48
+ host_ip_by_env = host_ipv4 or host_ipv6
49
+ host_ip_by_sdk = ray._private.services.get_node_ip_address()
50
+
51
+ host_ip = host_ip_by_env or host_ip_by_sdk
52
+ return host_ip
53
+
54
+ def _get_free_port(self) -> int:
55
+ with socket.socket() as sock:
56
+ sock.bind(("", 0))
57
+ return sock.getsockname()[1]
58
+
59
+ def get_availale_master_addr_port(self) -> Tuple[str, str]:
60
+ return self._get_node_ip(), str(self._get_free_port())
61
+
62
+ def _get_pid(self):
63
+ return
64
+
65
+
66
+ class WorkerMeta:
67
+ keys = [
68
+ "WORLD_SIZE",
69
+ "RANK",
70
+ "LOCAL_WORLD_SIZE",
71
+ "LOCAL_RANK",
72
+ "MASTER_ADDR",
73
+ "MASTER_PORT",
74
+ "CUDA_VISIBLE_DEVICES",
75
+ ]
76
+
77
+ def __init__(self, store) -> None:
78
+ self._store = store
79
+
80
+ def to_dict(self):
81
+ return {f"_{key.lower()}": self._store.get(f"_{key.lower()}", None) for key in WorkerMeta.keys}
82
+
83
+
84
+ # we assume that in each WorkerGroup, there is a Master Worker
85
+ class Worker(WorkerHelper):
86
+ """A (distributed) worker."""
87
+
88
+ _world_size: int
89
+ _rank: int
90
+ _local_world_size: int
91
+ _local_rank: int
92
+ _master_addr: str
93
+ _master_port: str
94
+ _cuda_visible_devices: str
95
+
96
+ def __new__(cls, *args, **kwargs):
97
+ instance = super().__new__(cls)
98
+
99
+ # note that here we use int to distinguish
100
+ disable_worker_init = int(os.getenv("DISABLE_WORKER_INIT", 0))
101
+ if disable_worker_init:
102
+ return instance
103
+
104
+ rank = os.getenv("RANK", None)
105
+ worker_group_prefix = os.getenv("WG_PREFIX", None)
106
+
107
+ # when decorator @ray.remote applies, __new__ will be called while we don't want to apply _configure_before_init
108
+ if None not in [rank, worker_group_prefix] and "ActorClass(" not in cls.__name__:
109
+ instance._configure_before_init(f"{worker_group_prefix}_register_center", int(rank))
110
+
111
+ return instance
112
+
113
+ def _configure_before_init(self, register_center_name: str, rank: int):
114
+ assert isinstance(rank, int), f"rank must be int, instead of {type(rank)}"
115
+
116
+ if rank == 0:
117
+ master_addr, master_port = self.get_availale_master_addr_port()
118
+ rank_zero_info = {
119
+ "MASTER_ADDR": master_addr,
120
+ "MASTER_PORT": master_port,
121
+ }
122
+ self.register_center = create_worker_group_register_center(name=register_center_name, info=rank_zero_info)
123
+ os.environ.update(rank_zero_info)
124
+
125
+ def __init__(self, cuda_visible_devices=None) -> None:
126
+ # construct a meta from envrionment variable. Note that the import must be inside the class because it is executed remotely
127
+ world_size = int(os.getenv("WORLD_SIZE"))
128
+ rank = int(os.getenv("RANK"))
129
+ self._rank = rank
130
+ self._world_size = world_size
131
+
132
+ if "AMD" in torch.cuda.get_device_name():
133
+ os.environ["CUDA_VISIBLE_DEVICES"] = os.getenv("ROCR_VISIBLE_DEVICES")
134
+ os.environ["LOCAL_RANK"] = os.getenv("RAY_LOCAL_RANK")
135
+ cuda_visible_devices = os.getenv("LOCAL_RANK", "0")
136
+ torch.cuda.set_device(int(cuda_visible_devices))
137
+
138
+ master_addr = os.getenv("MASTER_ADDR")
139
+ master_port = os.getenv("MASTER_PORT")
140
+
141
+ local_world_size = int(os.getenv("LOCAL_WORLD_SIZE", "1"))
142
+ local_rank = int(os.getenv("LOCAL_RANK", "0"))
143
+
144
+ store = {
145
+ "_world_size": world_size,
146
+ "_rank": rank,
147
+ "_local_world_size": local_world_size,
148
+ "_local_rank": local_rank,
149
+ "_master_addr": master_addr,
150
+ "_master_port": master_port,
151
+ }
152
+ if cuda_visible_devices is not None:
153
+ store["_cuda_visible_devices"] = cuda_visible_devices
154
+
155
+ meta = WorkerMeta(store=store)
156
+ self._configure_with_meta(meta=meta)
157
+
158
+ def _configure_with_meta(self, meta: WorkerMeta):
159
+ """
160
+ This function should only be called inside by WorkerGroup
161
+ """
162
+ assert isinstance(meta, WorkerMeta)
163
+ self.__dict__.update(meta.to_dict()) # this is hacky
164
+ # print(f"__dict__: {self.__dict__}")
165
+ for key in WorkerMeta.keys:
166
+ val = self.__dict__.get(f"_{key.lower()}", None)
167
+ if val is not None:
168
+ # print(f"set {key} to {val}")
169
+ os.environ[key] = str(val)
170
+
171
+ os.environ["REDIS_STORE_SERVER_HOST"] = (
172
+ str(self._master_addr).replace("[", "").replace("]", "") if self._master_addr else ""
173
+ )
174
+
175
+ def get_master_addr_port(self):
176
+ return self._master_addr, self._master_port
177
+
178
+ def get_cuda_visible_devices(self):
179
+ cuda_visible_devices = os.getenv("CUDA_VISIBLE_DEVICES", "not set")
180
+ return cuda_visible_devices
181
+
182
+ def print_rank0(self, *args, **kwargs):
183
+ if self.rank == 0:
184
+ print(*args, **kwargs)
185
+
186
+ @property
187
+ def world_size(self):
188
+ return self._world_size
189
+
190
+ @property
191
+ def rank(self):
192
+ return self._rank
193
+
194
+ @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO_WITH_FUNC)
195
+ def execute_with_func_generator(self, func, *args, **kwargs):
196
+ ret_proto = func(self, *args, **kwargs)
197
+ return ret_proto
198
+
199
+ @register(dispatch_mode=Dispatch.ALL_TO_ALL, execute_mode=Execute.RANK_ZERO)
200
+ def execute_func_rank_zero(self, func, *args, **kwargs):
201
+ result = func(*args, **kwargs)
202
+ return result
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/base/worker_group.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ the class of WorkerGroup
16
+ """
17
+
18
+ import logging
19
+ import signal
20
+ import threading
21
+ import time
22
+ from typing import Any, Callable, Dict, List, Optional
23
+
24
+ from .decorator import MAGIC_ATTR, Dispatch, get_predefined_dispatch_fn, get_predefined_execute_fn
25
+
26
+
27
+ class ResourcePool:
28
+ """The resource pool with meta info such as world size."""
29
+
30
+ def __init__(
31
+ self, process_on_nodes: Optional[Any] = None, max_colocate_count: int = 10, n_gpus_per_node: int = 8
32
+ ) -> None:
33
+ if process_on_nodes is None:
34
+ process_on_nodes = []
35
+
36
+ self._store = process_on_nodes
37
+ self.max_colocate_count = max_colocate_count
38
+ self.n_gpus_per_node = n_gpus_per_node # this is left for future huawei GPU that contains 16 GPUs per node
39
+
40
+ def add_node(self, process_count):
41
+ self._store.append(process_count)
42
+
43
+ @property
44
+ def world_size(self):
45
+ return sum(self._store)
46
+
47
+ def __call__(self) -> Any:
48
+ return self._store
49
+
50
+ @property
51
+ def store(self):
52
+ return self._store
53
+
54
+ def local_world_size_list(self) -> List[int]:
55
+ nested_local_world_size_list = [
56
+ [local_world_size for _ in range(local_world_size)] for local_world_size in self._store
57
+ ]
58
+ return [item for row in nested_local_world_size_list for item in row]
59
+
60
+ def local_rank_list(self) -> List[int]:
61
+ nested_local_rank_list = [[i for i in range(local_world_size)] for local_world_size in self._store] # noqa: C416
62
+ return [item for row in nested_local_rank_list for item in row]
63
+
64
+
65
+ class ClassWithInitArgs:
66
+ """
67
+ This class stores a class constructor and the args/kwargs to construct the class.
68
+ It is used to instantiate the remote class.
69
+ """
70
+
71
+ def __init__(self, cls, *args, **kwargs) -> None:
72
+ self.cls = cls
73
+ self.args = args
74
+ self.kwargs = kwargs
75
+
76
+ def __call__(self) -> Any:
77
+ return self.cls(*self.args, **self.kwargs)
78
+
79
+
80
+ def check_workers_alive(workers: List, is_alive: Callable, gap_time: float = 1) -> None:
81
+ while True:
82
+ for worker in workers:
83
+ if not is_alive(worker):
84
+ logging.warning(f"Worker {worker} is not alive, sending signal to main thread")
85
+ signal.raise_signal(signal.SIGABRT)
86
+
87
+ time.sleep(gap_time)
88
+
89
+
90
+ class WorkerGroup:
91
+ """A group of workers"""
92
+
93
+ def __init__(self, resource_pool: ResourcePool, **kwargs) -> None:
94
+ self._is_init_with_detached_workers = True if resource_pool is None else False
95
+
96
+ if resource_pool is not None:
97
+ # handle the case when WorkGroup is attached to an existing one
98
+ self._procecss_dispatch_config = resource_pool()
99
+ else:
100
+ self._procecss_dispatch_config = None
101
+
102
+ self._workers = []
103
+ self._worker_names = []
104
+
105
+ self._master_addr = None
106
+ self._master_port = None
107
+
108
+ self._checker_thread: threading.Thread = None
109
+
110
+ def _is_worker_alive(self, worker):
111
+ raise NotImplementedError("WorkerGroup._is_worker_alive called, should be implemented in derived class.")
112
+
113
+ def _block_until_all_workers_alive(self) -> None:
114
+ while True:
115
+ all_state = [self._is_worker_alive(worker) for worker in self._workers]
116
+ if False in all_state:
117
+ time.sleep(1)
118
+ else:
119
+ break
120
+
121
+ def start_worker_aliveness_check(self, every_n_seconds=1) -> None:
122
+ # before starting checking worker aliveness, make sure all workers are already alive
123
+ self._block_until_all_workers_alive()
124
+
125
+ self._checker_thread = threading.Thread(
126
+ target=check_workers_alive, args=(self._workers, self._is_worker_alive, every_n_seconds)
127
+ )
128
+ self._checker_thread.start()
129
+
130
+ @property
131
+ def world_size(self):
132
+ return len(self._workers)
133
+
134
+ def _bind_worker_method(self, user_defined_cls, func_generator):
135
+ """
136
+ Bind the worker method to the WorkerGroup
137
+ """
138
+ for method_name in dir(user_defined_cls):
139
+ try:
140
+ method = getattr(user_defined_cls, method_name)
141
+ assert callable(method), f"{method_name} in {user_defined_cls} is not callable"
142
+ except Exception:
143
+ # if it is a property, it will fail because Class doesn't have instance property
144
+ continue
145
+
146
+ if hasattr(method, MAGIC_ATTR):
147
+ # this method is decorated by register
148
+ attribute = getattr(method, MAGIC_ATTR)
149
+ assert isinstance(attribute, Dict), f"attribute must be a dictionary. Got {type(attribute)}"
150
+ assert "dispatch_mode" in attribute, "attribute must contain dispatch_mode in its key"
151
+
152
+ dispatch_mode = attribute["dispatch_mode"]
153
+ execute_mode = attribute["execute_mode"]
154
+ blocking = attribute["blocking"]
155
+
156
+ # get dispatch fn
157
+ if isinstance(dispatch_mode, Dispatch):
158
+ # get default dispatch fn
159
+ fn = get_predefined_dispatch_fn(dispatch_mode=dispatch_mode)
160
+ dispatch_fn = fn["dispatch_fn"]
161
+ collect_fn = fn["collect_fn"]
162
+ else:
163
+ assert isinstance(dispatch_mode, dict)
164
+ assert "dispatch_fn" in dispatch_mode
165
+ assert "collect_fn" in dispatch_mode
166
+ dispatch_fn = dispatch_mode["dispatch_fn"]
167
+ collect_fn = dispatch_mode["collect_fn"]
168
+
169
+ # get execute_fn_name
170
+ execute_mode = get_predefined_execute_fn(execute_mode=execute_mode)
171
+ wg_execute_fn_name = execute_mode["execute_fn_name"]
172
+
173
+ # get execute_fn from string
174
+ try:
175
+ execute_fn = getattr(self, wg_execute_fn_name)
176
+ assert callable(execute_fn), "execute_fn must be callable"
177
+ except Exception:
178
+ print(f"execute_fn {wg_execute_fn_name} is invalid")
179
+ raise
180
+
181
+ # bind a new method to the RayWorkerGroup
182
+ func = func_generator(
183
+ self,
184
+ method_name,
185
+ dispatch_fn=dispatch_fn,
186
+ collect_fn=collect_fn,
187
+ execute_fn=execute_fn,
188
+ blocking=blocking,
189
+ )
190
+
191
+ try:
192
+ setattr(self, method_name, func)
193
+ except Exception:
194
+ raise ValueError(f"Fail to set method_name {method_name}")
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .base import RayClassWithInitArgs, RayResourcePool, RayWorkerGroup, create_colocated_worker_cls
16
+
17
+
18
+ __all__ = ["RayClassWithInitArgs", "RayResourcePool", "RayWorkerGroup", "create_colocated_worker_cls"]
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/single_controller/ray/base.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import random
17
+ import re
18
+ import string
19
+ import time
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+ from unittest.mock import patch
22
+
23
+ import ray
24
+ from ray.actor import ActorHandle
25
+ from ray.experimental.state.api import get_actor
26
+ from ray.util import list_named_actors
27
+ from ray.util.placement_group import PlacementGroup, placement_group
28
+ from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy, PlacementGroupSchedulingStrategy
29
+
30
+ from ..base import ClassWithInitArgs, ResourcePool, Worker, WorkerGroup
31
+ from ..base.decorator import MAGIC_ATTR
32
+
33
+
34
+ __all__ = ["Worker"]
35
+
36
+
37
+ def get_random_string(length: int) -> str:
38
+ letters_digits = string.ascii_letters + string.digits
39
+ return "".join(random.choice(letters_digits) for _ in range(length))
40
+
41
+
42
+ def func_generator(self, method_name, dispatch_fn, collect_fn, execute_fn, blocking):
43
+ def func(*args, **kwargs):
44
+ args, kwargs = dispatch_fn(self, *args, **kwargs)
45
+ output = execute_fn(method_name, *args, **kwargs)
46
+ if blocking:
47
+ output = ray.get(output)
48
+ output = collect_fn(self, output)
49
+ return output
50
+
51
+ return func
52
+
53
+
54
+ def sort_placement_group_by_node_ip(pgs: List[PlacementGroup]) -> List[PlacementGroup]:
55
+ """
56
+ Sort the placement groups by node ip, all bundles in a single placement group should be on the same node.
57
+
58
+ FSDPCheckpointManager saves sharded model states and optimizer states in local storage, which requires RANK
59
+ to be consistent across nodes when resume from checkpoint.
60
+
61
+ With this function, if there's only one resource pool and there's no node change, RANK should be consistent
62
+ across nodes in multiple ray jobs, even if the whole ray cluster is restarted.
63
+ """
64
+ node_ip = {node["NodeID"]: node["NodeManagerAddress"] for node in ray.nodes()}
65
+ pg_ip = {}
66
+ for pg in pgs:
67
+ specs = ray._private.state.state.placement_group_table(pg.id)
68
+ # all bunles should be on the same node
69
+ node_id = specs["bundles_to_node_id"][0]
70
+ pg_ip[pg.id] = node_ip[node_id]
71
+
72
+ return sorted(pgs, key=lambda pg: pg_ip[pg.id])
73
+
74
+
75
+ class RayResourcePool(ResourcePool):
76
+ def __init__(
77
+ self,
78
+ process_on_nodes: List[int] = None,
79
+ use_gpu: bool = True,
80
+ name_prefix: str = "",
81
+ max_colocate_count: int = 5,
82
+ detached: bool = False,
83
+ ) -> None:
84
+ super().__init__(process_on_nodes, max_colocate_count)
85
+ self.use_gpu = use_gpu
86
+ # print(f"in RayProcessDispatchConfiguration: name_prefix = {name_prefix}")
87
+ self.name_prefix = name_prefix
88
+ self.pgs = None
89
+ self.detached = detached
90
+
91
+ def get_placement_groups(self, strategy: str = "STRICT_PACK", name: Optional[str] = None) -> List[PlacementGroup]:
92
+ if self.pgs is not None:
93
+ return self.pgs
94
+
95
+ pg_name_prefix = (
96
+ name if name else f"{self.name_prefix}verl_group_{'_'.join([str(count) for count in self._store])}:"
97
+ )
98
+ # print(f"pg_name_prefix = {pg_name_prefix}")
99
+ pg_scheme = [
100
+ [
101
+ {"CPU": self.max_colocate_count, "GPU": 1} if self.use_gpu else {"CPU": self.max_colocate_count}
102
+ for _ in range(process_count)
103
+ ]
104
+ for process_count in self._store
105
+ ]
106
+
107
+ lifetime = "detached" if self.detached else None
108
+
109
+ pgs = [
110
+ placement_group(bundles=bundles, strategy=strategy, name=pg_name_prefix + str(idx), lifetime=lifetime)
111
+ for idx, bundles in enumerate(pg_scheme)
112
+ ]
113
+
114
+ ray.get([pg.ready() for pg in pgs])
115
+
116
+ self.pgs = pgs
117
+ return pgs
118
+
119
+
120
+ def extract_pg_from_exist(
121
+ resource_pools: Dict[str, RayResourcePool], src_role_names: List[str], resource_pool: RayResourcePool
122
+ ) -> List[PlacementGroup]:
123
+ src_pgs = [
124
+ pg
125
+ for role_name, resource_pool in resource_pools.items()
126
+ for pg in resource_pool.get_placement_groups()
127
+ if role_name in src_role_names
128
+ ]
129
+
130
+ sorted_src_pgs = sorted(src_pgs, key=lambda pg: pg.bundle_count, reverse=True)
131
+ sorted_process_on_nodes = sorted([(val, idx) for idx, val in enumerate(resource_pool.store)], reverse=True)
132
+
133
+ unsorted_pgs: List[Tuple[int, PlacementGroup]] = []
134
+ searching_idx = 0
135
+ for request_process, original_idx in sorted_process_on_nodes:
136
+ assert searching_idx < len(sorted_src_pgs), f"no enough nodes for request: searching {searching_idx} th node"
137
+ assert request_process <= sorted_src_pgs[searching_idx].bundle_count, (
138
+ f"requesting {request_process} processes, bundle count cannot satisfy"
139
+ )
140
+ unsorted_pgs.append((original_idx, sorted_src_pgs[searching_idx]))
141
+ searching_idx += 1
142
+
143
+ return [pg for _, pg in sorted(unsorted_pgs)]
144
+
145
+
146
+ def merge_resource_pool(rp1: RayResourcePool, rp2: RayResourcePool) -> RayResourcePool:
147
+ assert rp1.use_gpu == rp2.use_gpu, "Both RayResourcePool must either use_gpu or not"
148
+ assert rp1.max_colocate_count == rp2.max_colocate_count, (
149
+ "Both RayResourcePool must has the same max_colocate_count"
150
+ )
151
+ assert rp1.n_gpus_per_node == rp2.n_gpus_per_node, "Both RayResourcePool must has the same n_gpus_per_node"
152
+ assert rp1.detached == rp2.detached, "Detached ResourcePool cannot be merged with non-detached ResourcePool"
153
+
154
+ new_store = rp1.store + rp2.store
155
+
156
+ merged = RayResourcePool(new_store, rp1.use_gpu, f"{rp1.name_prefix}_{rp2.name_prefix}")
157
+ merged.pgs = rp1.get_placement_groups() + rp2.get_placement_groups()
158
+
159
+ return merged
160
+
161
+
162
+ class RayClassWithInitArgs(ClassWithInitArgs):
163
+ def __init__(self, cls, *args, **kwargs) -> None:
164
+ # self._options = kwargs.pop('options', dict())
165
+ super().__init__(cls, *args, **kwargs)
166
+ self._options = {}
167
+ self._additional_resource = {}
168
+
169
+ def set_additional_resource(self, additional_resource):
170
+ self._additional_resource = additional_resource
171
+
172
+ def update_options(self, options: Dict):
173
+ self._options.update(options)
174
+
175
+ def __call__(
176
+ self,
177
+ placement_group: PlacementGroup,
178
+ placement_group_bundle_idx: int,
179
+ use_gpu: bool = True,
180
+ num_gpus: int = 1,
181
+ sharing_with: Worker = None,
182
+ ) -> Any:
183
+ if sharing_with is not None:
184
+ target_node_id = ray.get(sharing_with.get_node_id.remote())
185
+ cuda_visible_devices = ray.get(sharing_with.get_cuda_visible_devices.remote())
186
+ options = {"scheduling_strategy": NodeAffinitySchedulingStrategy(node_id=target_node_id, soft=False)}
187
+ return self.cls.options(**options).remote(
188
+ *self.args, cuda_visible_devices=cuda_visible_devices, **self.kwargs
189
+ )
190
+
191
+ options = {
192
+ "scheduling_strategy": PlacementGroupSchedulingStrategy(
193
+ placement_group=placement_group, placement_group_bundle_index=placement_group_bundle_idx
194
+ )
195
+ }
196
+ options.update(self._options)
197
+
198
+ if use_gpu:
199
+ options["num_gpus"] = num_gpus
200
+
201
+ if len(self._additional_resource) > 1:
202
+ for k, v in self._additional_resource.items():
203
+ options[k] = v
204
+
205
+ # print("cls:", self.cls)
206
+ # print("args: ", self.args)
207
+ # print("kwargs: ", self.kwargs)
208
+ return self.cls.options(**options).remote(*self.args, **self.kwargs)
209
+
210
+
211
+ class RayWorkerGroup(WorkerGroup):
212
+ def __init__(
213
+ self,
214
+ resource_pool: RayResourcePool = None,
215
+ ray_cls_with_init: RayClassWithInitArgs = None,
216
+ bin_pack: bool = True,
217
+ name_prefix: str = None,
218
+ detached: bool = False,
219
+ worker_names: List[str] = None,
220
+ **kwargs,
221
+ ) -> None:
222
+ super().__init__(resource_pool=resource_pool, **kwargs)
223
+ self.ray_cls_with_init = ray_cls_with_init
224
+ self.name_prefix = get_random_string(length=6) if name_prefix is None else name_prefix
225
+
226
+ if worker_names is not None:
227
+ assert self._is_init_with_detached_workers
228
+ self._worker_names = worker_names
229
+
230
+ if self._is_init_with_detached_workers:
231
+ self._init_with_detached_workers(worker_names=worker_names)
232
+ else:
233
+ self._init_with_resource_pool(
234
+ resource_pool=resource_pool, ray_cls_with_init=ray_cls_with_init, bin_pack=bin_pack, detached=detached
235
+ )
236
+
237
+ if ray_cls_with_init is not None:
238
+ self._bind_worker_method(self.ray_cls_with_init.cls, func_generator)
239
+
240
+ def _is_worker_alive(self, worker: ActorHandle) -> bool:
241
+ worker_state_dict = get_actor(worker._actor_id.hex())
242
+ return worker_state_dict.get("state", "undefined") == "ALIVE" if worker_state_dict is not None else False
243
+
244
+ def _init_with_detached_workers(self, worker_names: List[str]) -> None:
245
+ workers = [ray.get_actor(name=name) for name in worker_names]
246
+ self._workers = workers
247
+ self._world_size = len(worker_names)
248
+
249
+ def _init_with_resource_pool(
250
+ self, resource_pool: RayResourcePool, ray_cls_with_init: RayClassWithInitArgs, bin_pack: bool, detached: bool
251
+ ):
252
+ use_gpu = resource_pool.use_gpu
253
+
254
+ strategy = "PACK"
255
+ if bin_pack:
256
+ strategy = "STRICT_PACK"
257
+
258
+ pgs = resource_pool.get_placement_groups(strategy=strategy)
259
+ world_size = resource_pool.world_size
260
+ self._world_size = world_size
261
+ # cia.add_kwarg("_world_size", world_size)
262
+ num_gpus = 1 / resource_pool.max_colocate_count
263
+
264
+ rank = -1
265
+ local_world_size = resource_pool.store[0]
266
+ for pg_idx, pg in enumerate(sort_placement_group_by_node_ip(pgs)):
267
+ assert local_world_size <= pg.bundle_count, f"when generating for {self.name_prefix}, for the "
268
+ for local_rank in range(local_world_size):
269
+ rank += 1
270
+
271
+ # we pass in environment variable at option so that Worker can use environment variable to set
272
+ env_vars = {
273
+ "WORLD_SIZE": str(world_size),
274
+ "RANK": str(rank),
275
+ "WG_PREFIX": self.name_prefix,
276
+ "WG_BACKEND": "ray",
277
+ "RAY_LOCAL_WORLD_SIZE": str(local_world_size),
278
+ "RAY_LOCAL_RANK": str(local_rank),
279
+ }
280
+ if rank != 0:
281
+ env_vars["MASTER_ADDR"] = self._master_addr
282
+ env_vars["MASTER_PORT"] = self._master_port
283
+
284
+ cia_name = type(ray_cls_with_init.cls).__name__
285
+ match = re.search(r"ActorClass\(([^)]+)\)", cia_name) # ray.remote(Obj) -> "ActorClass(Obj)"
286
+ cia_name = match.group(1) if match else cia_name # "ActorClass(Obj)" -> "Obj"
287
+ name = f"{self.name_prefix}{cia_name}_{pg_idx}:{local_rank}" # e.g. Worker_2:5
288
+
289
+ ray_cls_with_init.update_options({"runtime_env": {"env_vars": env_vars}, "name": name})
290
+
291
+ if detached:
292
+ ray_cls_with_init.update_options({"lifetime": "detached"})
293
+
294
+ # create a worker
295
+ worker = ray_cls_with_init(
296
+ placement_group=pg, placement_group_bundle_idx=local_rank, use_gpu=use_gpu, num_gpus=num_gpus
297
+ )
298
+ self._workers.append(worker)
299
+ self._worker_names.append(name)
300
+
301
+ if rank == 0:
302
+ register_center_actor = None
303
+ for _ in range(120):
304
+ if f"{self.name_prefix}_register_center" not in list_named_actors():
305
+ time.sleep(1)
306
+ else:
307
+ register_center_actor = ray.get_actor(f"{self.name_prefix}_register_center")
308
+ break
309
+ assert register_center_actor is not None, (
310
+ f"failed to get register_center_actor: {self.name_prefix}_register_center in {list_named_actors(all_namespaces=True)}"
311
+ )
312
+ rank_zero_info = ray.get(register_center_actor.get_rank_zero_info.remote())
313
+ self._master_addr, self._master_port = rank_zero_info["MASTER_ADDR"], rank_zero_info["MASTER_PORT"]
314
+ # print(f"rank_zero_info: {rank_zero_info}")
315
+ # print(f"master_addr: {self._master_addr}, master_port: {self._master_port}")
316
+
317
+ @property
318
+ def worker_names(self):
319
+ return self._worker_names
320
+
321
+ @classmethod
322
+ def from_detached(cls, worker_names=None, ray_cls_with_init=None):
323
+ worker_group = cls(
324
+ resource_pool=None, ray_cls_with_init=ray_cls_with_init, name_prefix=None, worker_names=worker_names
325
+ )
326
+ return worker_group
327
+
328
+ def spawn(self, prefix_set):
329
+ """
330
+ spawn to a dictionary of worker groups, each with a subset of method with prefix.
331
+
332
+ """
333
+
334
+ def _rebind_actor_methods(worker_group, actor_name):
335
+ """
336
+ bind the method with actor_prefix to its original name
337
+ """
338
+ prefix: str = actor_name + "_"
339
+ for method_name in dir(worker_group):
340
+ if method_name.startswith(prefix):
341
+ # only valid when Python >= 3.9
342
+ original_method_name = method_name.removeprefix(prefix)
343
+ method = getattr(worker_group, method_name)
344
+ setattr(worker_group, original_method_name, method)
345
+
346
+ new_worker_group_dict = {}
347
+ for prefix in prefix_set:
348
+ new_worker_group = self.from_detached(
349
+ worker_names=self._worker_names, ray_cls_with_init=self.ray_cls_with_init
350
+ )
351
+
352
+ _rebind_actor_methods(new_worker_group, prefix)
353
+ new_worker_group_dict[prefix] = new_worker_group
354
+ return new_worker_group_dict
355
+
356
+ def execute_rank_zero_sync(self, method_name: str, *args, **kwargs):
357
+ return ray.get(self.execute_rank_zero_async(method_name, *args, **kwargs))
358
+
359
+ def execute_rank_zero_async(self, method_name: str, *args, **kwargs):
360
+ remote_call = getattr(self._workers[0], method_name)
361
+ return remote_call.remote(*args, **kwargs)
362
+
363
+ def execute_rank_zero(self, method_name: str, *args, **kwargs):
364
+ return self.execute_rank_zero_async(method_name, *args, **kwargs)
365
+
366
+ def execute_all(self, method_name: str, *args, **kwargs):
367
+ return self.execute_all_async(method_name, *args, **kwargs)
368
+
369
+ def execute_all_sync(self, method_name: str, *args, **kwargs):
370
+ return ray.get(self.execute_all_async(method_name, *args, **kwargs))
371
+
372
+ def execute_all_async(self, method_name: str, *args, **kwargs):
373
+ # Here we assume that if all the parameters in args and kwargs are lists,
374
+ # and the lengths of all these lists are the same as len(self._workers),
375
+ # then we will send each element in the list to the corresponding worker.
376
+ # print(f"execute_all_async: method {method_name}({args}, {kwargs})")
377
+ length = len(self._workers)
378
+ if all(isinstance(arg, list) for arg in args) and all(isinstance(kwarg, list) for kwarg in kwargs.values()):
379
+ if all(len(arg) == length for arg in args) and all(len(kwarg) == length for kwarg in kwargs.values()):
380
+ # print(f"splitting args and kwargs into {length} shards")
381
+ result = []
382
+ for i in range(length):
383
+ sliced_args = tuple(arg[i] for arg in args)
384
+ sliced_kwargs = {k: v[i] for k, v in kwargs.items()}
385
+ remote_call = getattr(self._workers[i], method_name)
386
+ result.append(remote_call.remote(*sliced_args, **sliced_kwargs))
387
+ return result
388
+
389
+ return [getattr(worker, method_name).remote(*args, **kwargs) for worker in self._workers]
390
+
391
+ @property
392
+ def master_address(self):
393
+ return self._master_addr
394
+
395
+ @property
396
+ def master_port(self):
397
+ return self._master_port
398
+
399
+ @property
400
+ def workers(self):
401
+ return self._workers
402
+
403
+ @property
404
+ def world_size(self):
405
+ return self._world_size
406
+
407
+
408
+ """
409
+ Utilities that enables creating workers inside the same ray.Actor,
410
+ with code written in separate ray.Actors.
411
+ """
412
+
413
+
414
+ def _bind_workers_method_to_parent(cls, key, user_defined_cls):
415
+ """
416
+ Binds the methods of each worker to the WorkerDict.
417
+ Note that we only bind public methods that are decorated by register
418
+ """
419
+ for method_name in dir(user_defined_cls):
420
+ try:
421
+ method = getattr(user_defined_cls, method_name)
422
+ assert callable(method), f"{method_name} in {user_defined_cls} is not callable"
423
+ except Exception:
424
+ # if it is a property, it will fail because Class doesn't have instance property
425
+ continue
426
+
427
+ if hasattr(method, MAGIC_ATTR):
428
+
429
+ def generate_function(name):
430
+ def func(self, *args, **kwargs):
431
+ # dispatch to the actual worker
432
+ return getattr(self.worker_dict[key], name)(*args, **kwargs)
433
+
434
+ return func
435
+
436
+ func = generate_function(method_name)
437
+ # pass MAGIC_ATTR for outer worker group
438
+ setattr(func, MAGIC_ATTR, getattr(method, MAGIC_ATTR))
439
+ try:
440
+ method_name_with_prefix = key + "_" + method_name
441
+ setattr(cls, method_name_with_prefix, func)
442
+ # print(f'Binding {method_name_with_prefix}')
443
+ except Exception:
444
+ raise ValueError(f"Fail to set method_name {method_name}")
445
+
446
+
447
+ def _unwrap_ray_remote(cls):
448
+ if hasattr(cls, "__ray_actor_class__"):
449
+ cls = cls.__ray_actor_class__
450
+ return cls
451
+
452
+
453
+ def create_colocated_worker_cls(class_dict: dict[str, RayClassWithInitArgs]):
454
+ """
455
+ This function should return a class instance that delegates the calls to every
456
+ cls in cls_dict
457
+ """
458
+ cls_dict = {}
459
+ init_args_dict = {}
460
+ worker_cls = None
461
+ for key, cls in class_dict.items():
462
+ if worker_cls is None:
463
+ worker_cls = cls.cls.__ray_actor_class__.__base__
464
+ else:
465
+ assert worker_cls == cls.cls.__ray_actor_class__.__base__, (
466
+ "the worker class should be the same when share the same process"
467
+ )
468
+ cls_dict[key] = cls.cls
469
+ init_args_dict[key] = {"args": cls.args, "kwargs": cls.kwargs}
470
+
471
+ assert cls_dict.keys() == init_args_dict.keys()
472
+
473
+ # TODO: create a class with customizable name
474
+ class WorkerDict(worker_cls):
475
+ def __init__(self):
476
+ super().__init__()
477
+ self.worker_dict = {}
478
+ for key, user_defined_cls in cls_dict.items():
479
+ user_defined_cls = _unwrap_ray_remote(user_defined_cls)
480
+ # directly instantiate the class without remote
481
+ with patch.dict(os.environ, {"DISABLE_WORKER_INIT": "1"}):
482
+ self.worker_dict[key] = user_defined_cls(
483
+ *init_args_dict[key].get("args", ()), **init_args_dict[key].get("kwargs", {})
484
+ )
485
+
486
+ # now monkey-patch the methods from inner class to WorkerDict
487
+ for key, user_defined_cls in cls_dict.items():
488
+ user_defined_cls = _unwrap_ray_remote(user_defined_cls)
489
+ _bind_workers_method_to_parent(WorkerDict, key, user_defined_cls)
490
+
491
+ remote_cls = ray.remote(WorkerDict)
492
+ remote_cls = RayClassWithInitArgs(cls=remote_cls)
493
+ return remote_cls
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/config.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ PPO config
16
+ """
17
+
18
+ import os
19
+ from dataclasses import asdict, dataclass, field, fields, is_dataclass
20
+ from typing import Optional, Tuple
21
+
22
+ from ..workers.config import WorkerConfig
23
+
24
+
25
+ def recursive_post_init(dataclass_obj):
26
+ if hasattr(dataclass_obj, "post_init"):
27
+ dataclass_obj.post_init()
28
+
29
+ for attr in fields(dataclass_obj):
30
+ if is_dataclass(getattr(dataclass_obj, attr.name)):
31
+ recursive_post_init(getattr(dataclass_obj, attr.name))
32
+
33
+
34
+ @dataclass
35
+ class DataConfig:
36
+ train_files: str = ""
37
+ val_files: str = ""
38
+ prompt_key: str = "prompt"
39
+ answer_key: str = "answer"
40
+ image_key: str = "images"
41
+ max_prompt_length: int = 512
42
+ max_response_length: int = 512
43
+ rollout_batch_size: int = 512
44
+ val_batch_size: int = -1
45
+ format_prompt: Optional[str] = None
46
+ override_chat_template: Optional[str] = None
47
+ shuffle: bool = True
48
+ seed: int = 1
49
+ max_pixels: int = 4194304
50
+ min_pixels: int = 262144
51
+ filter_overlong_prompts: bool = True
52
+
53
+ def post_init(self):
54
+ if self.format_prompt is not None:
55
+ if os.path.exists(self.format_prompt): # ray job uses absolute path
56
+ self.format_prompt = os.path.abspath(self.format_prompt)
57
+ else:
58
+ self.format_prompt = None
59
+
60
+
61
+ @dataclass
62
+ class AlgorithmConfig:
63
+ gamma: float = 1.0
64
+ lam: float = 1.0
65
+ adv_estimator: str = "grpo"
66
+ disable_kl: bool = False
67
+ use_kl_loss: bool = False
68
+ kl_penalty: str = "kl"
69
+ kl_coef: float = 1e-3
70
+ kl_type: str = "fixed"
71
+ kl_horizon: float = 0.0
72
+ kl_target: float = 0.0
73
+
74
+
75
+ @dataclass
76
+ class TrainerConfig:
77
+ total_epochs: int = 10
78
+ max_steps: Optional[int] = None
79
+ project_name: str = "easy_r1"
80
+ experiment_name: str = "demo"
81
+ logger: Tuple[str] = ("console", "wandb")
82
+ nnodes: int = 1
83
+ n_gpus_per_node: int = 8
84
+ critic_warmup: int = 0
85
+ val_freq: int = -1
86
+ val_before_train: bool = True
87
+ val_only: bool = False
88
+ val_generations_to_log: int = 0
89
+ save_freq: int = -1
90
+ save_limit: int = -1
91
+ save_checkpoint_path: Optional[str] = None
92
+ load_checkpoint_path: Optional[str] = None
93
+
94
+ def post_init(self):
95
+ if self.save_checkpoint_path is None:
96
+ self.save_checkpoint_path = os.path.join("checkpoints", self.project_name, self.experiment_name)
97
+
98
+ self.save_checkpoint_path = os.path.abspath(self.save_checkpoint_path) # ray job uses absolute path
99
+ if self.load_checkpoint_path is not None:
100
+ self.load_checkpoint_path = os.path.abspath(self.load_checkpoint_path)
101
+
102
+
103
+ @dataclass
104
+ class PPOConfig:
105
+ data: DataConfig = field(default_factory=DataConfig)
106
+ worker: WorkerConfig = field(default_factory=WorkerConfig)
107
+ algorithm: AlgorithmConfig = field(default_factory=AlgorithmConfig)
108
+ trainer: TrainerConfig = field(default_factory=TrainerConfig)
109
+
110
+ def post_init(self):
111
+ self.worker.rollout.prompt_length = self.data.max_prompt_length
112
+ self.worker.rollout.response_length = self.data.max_response_length
113
+ self.worker.rollout.trust_remote_code = self.worker.actor.model.trust_remote_code
114
+ self.worker.actor.disable_kl = self.algorithm.disable_kl
115
+ self.worker.actor.use_kl_loss = self.algorithm.use_kl_loss
116
+ self.worker.actor.kl_penalty = self.algorithm.kl_penalty
117
+ self.worker.actor.kl_coef = self.algorithm.kl_coef
118
+
119
+ def deep_post_init(self):
120
+ recursive_post_init(self)
121
+
122
+ def to_dict(self):
123
+ return asdict(self)
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/core_algos.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2022 The HuggingFace Team
2
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """
16
+ Core functions to implement PPO algorithms.
17
+ The function implemented in this file should be used by trainer with different distributed strategies to
18
+ implement PPO
19
+ """
20
+
21
+ from abc import ABC, abstractmethod
22
+ from collections import defaultdict
23
+ from typing import TYPE_CHECKING, Tuple
24
+
25
+ import numpy as np
26
+ import torch
27
+ import torch.nn.functional as F
28
+
29
+ from ..utils import torch_functional as VF
30
+
31
+
32
+ if TYPE_CHECKING:
33
+ from .config import AlgorithmConfig
34
+
35
+
36
+ class KLController(ABC):
37
+ kl_coef: float
38
+ """KL coefficient."""
39
+
40
+ @abstractmethod
41
+ def update(self, current_kl: float, n_steps: int) -> None:
42
+ """Update kl_coef according to current KL."""
43
+ ...
44
+
45
+
46
+ class AdaptiveKLController(KLController):
47
+ """Adaptive KL controller described in: https://arxiv.org/pdf/1909.08593.pdf
48
+
49
+ Copied from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/utils.py#L54"""
50
+
51
+ def __init__(self, init_kl_coef: float, target_kl: float, horizon: float):
52
+ self.kl_coef = init_kl_coef
53
+ self.target = target_kl
54
+ self.horizon = horizon
55
+
56
+ def update(self, current_kl: float, n_steps: int) -> None:
57
+ target = self.target
58
+ proportional_error = np.clip(current_kl / target - 1, -0.2, 0.2)
59
+ mult = 1 + proportional_error * n_steps / self.horizon
60
+ self.kl_coef *= mult
61
+
62
+
63
+ class FixedKLController(KLController):
64
+ """Fixed KL controller.
65
+
66
+ Copeid from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/utils.py#L72"""
67
+
68
+ def __init__(self, init_kl_coef: float):
69
+ self.kl_coef = init_kl_coef
70
+
71
+ def update(self, current_kl: float, n_steps: int) -> None:
72
+ pass
73
+
74
+
75
+ def get_kl_controller(algorithm_config: "AlgorithmConfig") -> KLController:
76
+ """Adapted from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L319"""
77
+ if algorithm_config.kl_type == "fixed":
78
+ kl_ctrl = FixedKLController(init_kl_coef=algorithm_config.kl_coef)
79
+ elif algorithm_config.kl_type == "adaptive":
80
+ assert algorithm_config.kl_horizon > 0, f"horizon must be larger than 0. Got {algorithm_config.kl_horizon}."
81
+ kl_ctrl = AdaptiveKLController(
82
+ init_kl_coef=algorithm_config.kl_coef,
83
+ target_kl=algorithm_config.kl_target,
84
+ horizon=algorithm_config.kl_horizon,
85
+ )
86
+ else:
87
+ raise ValueError(f"Unknown kl type: {algorithm_config.kl_type}.")
88
+
89
+ return kl_ctrl
90
+
91
+
92
+ @torch.no_grad()
93
+ def compute_gae_advantage_return(
94
+ token_level_rewards: torch.Tensor,
95
+ values: torch.Tensor,
96
+ response_mask: torch.Tensor,
97
+ gamma: torch.Tensor,
98
+ lam: torch.Tensor,
99
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
100
+ """Adapted from https://github.com/huggingface/trl/blob/v0.16.0/trl/trainer/ppo_trainer.py#L513
101
+
102
+ Args:
103
+ token_level_rewards: `(torch.Tensor)`
104
+ shape: (bs, response_length)
105
+ values: `(torch.Tensor)`
106
+ shape: (bs, response_length)
107
+ response_mask: `(torch.Tensor)`
108
+ shape: (bs, response_length). The token after eos tokens have mask zero.
109
+ gamma: `(float)`
110
+ discounted factor used in RL
111
+ lam: `(float)`
112
+ lambda value when computing Generalized Advantage Estimation (https://arxiv.org/abs/1506.02438)
113
+
114
+ Returns:
115
+ advantages: `(torch.Tensor)`
116
+ shape: (bs, response_length)
117
+ returns: `(torch.Tensor)`
118
+ shape: (bs, response_length)
119
+
120
+ """
121
+ lastgaelam = 0
122
+ advantages_reversed = []
123
+ gen_len = token_level_rewards.shape[-1]
124
+ for t in reversed(range(gen_len)):
125
+ nextvalues = values[:, t + 1] if t < gen_len - 1 else 0.0
126
+ delta = token_level_rewards[:, t] + gamma * nextvalues - values[:, t]
127
+ lastgaelam = delta + gamma * lam * lastgaelam
128
+ advantages_reversed.append(lastgaelam)
129
+
130
+ advantages = torch.stack(advantages_reversed[::-1], dim=1)
131
+ returns = advantages + values
132
+ advantages = VF.masked_whiten(advantages, response_mask)
133
+ return advantages, returns
134
+
135
+
136
+ # NOTE(sgm): this implementation only consider outcome supervision, where the reward is a scalar.
137
+ @torch.no_grad()
138
+ def compute_grpo_outcome_advantage(
139
+ token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor, eps: float = 1e-6
140
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
141
+ """
142
+ Compute advantage for GRPO, operating only on Outcome reward
143
+ (with only one scalar reward for each response).
144
+
145
+ Args:
146
+ token_level_rewards: `(torch.Tensor)`
147
+ shape: (bs, response_length)
148
+ response_mask: `(torch.Tensor)`
149
+ shape: (bs, response_length)
150
+
151
+ Returns:
152
+ advantages: `(torch.Tensor)`
153
+ shape: (bs, response_length)
154
+ returns: `(torch.Tensor)`
155
+ shape: (bs, response_length)
156
+
157
+ """
158
+ scores = token_level_rewards.sum(dim=-1)
159
+ id2score = defaultdict(list)
160
+ id2mean, id2std = {}, {}
161
+
162
+ bsz = scores.shape[0]
163
+ for i in range(bsz):
164
+ id2score[index[i]].append(scores[i])
165
+
166
+ for idx in id2score:
167
+ assert len(id2score[idx]) > 1, "GRPO needs rollout.n > 1."
168
+ id2mean[idx] = torch.mean(torch.tensor(id2score[idx]))
169
+ id2std[idx] = torch.std(torch.tensor(id2score[idx]))
170
+
171
+ for i in range(bsz):
172
+ scores[i] = (scores[i] - id2mean[index[i]]) / (id2std[index[i]] + eps)
173
+
174
+ returns = scores.unsqueeze(-1) * response_mask
175
+ return returns, returns
176
+
177
+
178
+ @torch.no_grad()
179
+ def compute_rloo_outcome_advantage(
180
+ token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor
181
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
182
+ """
183
+ Compute advantage for RLOO based on https://arxiv.org/abs/2402.14740
184
+
185
+ Args:
186
+ token_level_rewards: `(torch.Tensor)`
187
+ shape: (bs, response_length)
188
+ response_mask: `(torch.Tensor)`
189
+ shape: (bs, response_length)
190
+
191
+ Returns:
192
+ advantages: `(torch.Tensor)`
193
+ shape: (bs, response_length)
194
+ returns: `(torch.Tensor)`
195
+ shape: (bs, response_length)
196
+
197
+ """
198
+ scores = token_level_rewards.sum(dim=-1)
199
+
200
+ id2score = defaultdict(list)
201
+ id2sum = {}
202
+ bsz = scores.shape[0]
203
+ for i in range(bsz):
204
+ id2score[index[i]].append(scores[i])
205
+
206
+ for idx in id2score:
207
+ id2sum[idx] = torch.sum(torch.tensor(id2score[idx]))
208
+
209
+ for i in range(bsz):
210
+ sample_num = len(id2score[index[i]])
211
+ assert sample_num > 1, "RLOO needs rollout.n > 1."
212
+ baseline = (id2sum[index[i]] - scores[i]) / (sample_num - 1)
213
+ scores[i] = scores[i] - baseline
214
+
215
+ returns = scores.unsqueeze(-1) * response_mask
216
+ return returns, returns
217
+
218
+
219
+ @torch.no_grad()
220
+ def compute_reinforce_plus_plus_outcome_advantage(
221
+ token_level_rewards: torch.Tensor, response_mask: torch.Tensor, gamma: torch.Tensor
222
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
223
+ """
224
+ Compute advantage for REINFORCE++.
225
+ This implementation is based on the paper: https://arxiv.org/abs/2501.03262
226
+
227
+ Args:
228
+ token_level_rewards: `(torch.Tensor)`
229
+ shape: (bs, response_length)
230
+ response_mask: `(torch.Tensor)`
231
+ shape: (bs, response_length)
232
+
233
+ Returns:
234
+ advantages: `(torch.Tensor)`
235
+ shape: (bs, response_length)
236
+ returns: `(torch.Tensor)`
237
+ shape: (bs, response_length)
238
+
239
+ """
240
+ returns = torch.zeros_like(token_level_rewards)
241
+ running_return = 0
242
+ for t in reversed(range(token_level_rewards.shape[1])):
243
+ running_return = token_level_rewards[:, t] + gamma * running_return
244
+ returns[:, t] = running_return
245
+ # Reset after EOS
246
+ running_return = running_return * response_mask[:, t]
247
+
248
+ advantages = VF.masked_whiten(returns, response_mask)
249
+ return advantages, returns
250
+
251
+
252
+ @torch.no_grad()
253
+ def compute_remax_outcome_advantage(
254
+ token_level_rewards: torch.Tensor, reward_baselines: torch.Tensor, response_mask: torch.Tensor
255
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
256
+ """
257
+ Compute advantage for ReMax, operating only on Outcome reward
258
+ This implementation is based on the paper: https://arxiv.org/abs/2310.10505
259
+
260
+ (with only one scalar reward for each response).
261
+ Args:
262
+ token_level_rewards: `(torch.Tensor)`
263
+ shape: (bs, response_length)
264
+ reward_baselines: `(torch.Tensor)`
265
+ shape: (bs,)
266
+ response_mask: `(torch.Tensor)`
267
+ shape: (bs, response_length)
268
+
269
+ Returns:
270
+ advantages: `(torch.Tensor)`
271
+ shape: (bs, response_length)
272
+ returns: `(torch.Tensor)`
273
+ shape: (bs, response_length)
274
+
275
+ """
276
+ scores = token_level_rewards.sum(dim=-1) - reward_baselines
277
+ returns = scores.unsqueeze(-1) * response_mask
278
+ return returns, returns
279
+
280
+
281
+ def compute_rewards(
282
+ token_level_scores: torch.Tensor,
283
+ log_probs: torch.Tensor,
284
+ ref_log_probs: torch.Tensor,
285
+ kl_ratio: float,
286
+ ) -> torch.Tensor:
287
+ kl = log_probs - ref_log_probs
288
+ return token_level_scores - kl * kl_ratio
289
+
290
+
291
+ def compute_policy_loss(
292
+ old_log_probs: torch.Tensor,
293
+ log_probs: torch.Tensor,
294
+ advantages: torch.Tensor,
295
+ response_mask: torch.Tensor,
296
+ clip_ratio_low: float,
297
+ clip_ratio_high: float,
298
+ clip_ratio_dual: float,
299
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
300
+ """Compute the policy loss.
301
+
302
+ Adapted from https://github.com/huggingface/trl/blob/v0.15.0/trl/trainer/ppo_trainer.py#L568
303
+
304
+ Args:
305
+ old_log_prob: `(torch.Tensor)`
306
+ shape: (bs, response_length)
307
+ log_prob: `(torch.Tensor)`
308
+ shape: (bs, response_length)
309
+ advantages: `(torch.Tensor)`
310
+ shape: (bs, response_length)
311
+ response_mask: `(torch.Tensor)`
312
+ shape: (bs, response_length)
313
+ clip_ratio_low: (float)
314
+ The lower clip range used in PPO. See https://arxiv.org/abs/1707.06347
315
+ clip_ratio_high: (float)
316
+ The higher clip range used in DAPO. See https://arxiv.org/pdf/2503.14476
317
+ clip_ratio_dual: (float)
318
+ The dual clip range used in Dual-clip PPO. See https://arxiv.org/pdf/1912.09729
319
+
320
+ Returns:
321
+ pg_loss: `a scalar torch.Tensor`
322
+ policy gradient loss computed via PPO
323
+ pg_clipfrac_higher: (float)
324
+ a float number indicating the fraction of policy gradient loss being clipped to a higher value
325
+ pg_clipfrac_lower: (float)
326
+ a float number indicating the fraction of policy gradient loss being clipped to a lower value
327
+ ppo_kl: (float)
328
+ a float number indicating the mean KL divergence between the old policy and the new policy
329
+
330
+ """
331
+ negative_approx_kl = log_probs - old_log_probs
332
+ # clamp the ratio before exp to avoid nan
333
+ # see: https://github.com/pytorch/pytorch/issues/10729
334
+ ratio = torch.exp(negative_approx_kl)
335
+ clipped_ratio = torch.exp(
336
+ torch.clamp(negative_approx_kl, np.log(1.0 - clip_ratio_low), np.log(1.0 + clip_ratio_high))
337
+ )
338
+
339
+ pg_loss = -advantages * ratio
340
+ pg_loss2 = -advantages * clipped_ratio
341
+ pg_loss3 = -advantages * clip_ratio_dual
342
+
343
+ clipped_pg_loss_higher = torch.max(pg_loss, pg_loss2) # clip if pg_loss < pg_loss2
344
+ pg_clipfrac_higher = (pg_loss < pg_loss2).float()
345
+ clipped_pg_loss_lower = torch.min(clipped_pg_loss_higher, pg_loss3) # clip if pg_loss > pg_loss3 and adv < 0
346
+ final_pg_loss = torch.where(advantages < 0, clipped_pg_loss_lower, clipped_pg_loss_higher)
347
+ pg_clipfrac_lower = (clipped_pg_loss_higher > pg_loss3).float() * (advantages < 0).float()
348
+
349
+ final_pg_loss = VF.masked_mean(final_pg_loss, response_mask)
350
+ pg_clipfrac_higher = VF.masked_mean(pg_clipfrac_higher, response_mask)
351
+ pg_clipfrac_lower = VF.masked_mean(pg_clipfrac_lower, response_mask)
352
+ ppo_kl = VF.masked_mean(-negative_approx_kl, response_mask)
353
+ return final_pg_loss, pg_clipfrac_higher, pg_clipfrac_lower, ppo_kl
354
+
355
+
356
+ def compute_value_loss(
357
+ vpreds: torch.Tensor,
358
+ returns: torch.Tensor,
359
+ values: torch.Tensor,
360
+ action_mask: torch.Tensor,
361
+ cliprange_value: float,
362
+ ) -> Tuple[torch.Tensor, float]:
363
+ """Compute the value loss.
364
+
365
+ Adapted from https://github.com/huggingface/trl/blob/v0.15.0/trl/trainer/ppo_trainer.py#L556
366
+
367
+ Args:
368
+ vpreds (`torch.FloatTensor`):
369
+ Predicted values of the value head, shape (`batch_size`, `response_length`)
370
+ returns: (`torch.FloatTensor`):
371
+ Ground truth returns, shape (`batch_size`, `response_length`)
372
+ values (`torch.FloatTensor`):
373
+ Old values of value head, shape (`batch_size`, `response_length`)
374
+ action_mask: `(torch.Tensor)`
375
+ shape: (bs, response_length)
376
+ cliprange_value: (float)
377
+ The clip range for value net used in PPO. See https://arxiv.org/abs/1707.06347
378
+
379
+ Returns:
380
+ vf_loss: a scalar (`torch.FloatTensor`):
381
+ value function loss
382
+ vf_clipfrac: a float
383
+ The ratio of vf being clipped
384
+
385
+ """
386
+ vpredclipped = torch.clamp(vpreds, values - cliprange_value, values + cliprange_value)
387
+ vf_loss1 = torch.square(vpreds - returns)
388
+ vf_loss2 = torch.square(vpredclipped - returns)
389
+ vf_loss = 0.5 * VF.masked_mean(torch.max(vf_loss1, vf_loss2), action_mask) # clip if vf_loss1 < vf_loss2
390
+ vf_clipfrac = VF.masked_mean((vf_loss1 < vf_loss2).float(), action_mask)
391
+ return vf_loss, vf_clipfrac
392
+
393
+
394
+ def compute_kl(log_probs: torch.FloatTensor, ref_log_probs: torch.FloatTensor, kl_penalty: str) -> torch.Tensor:
395
+ """Compute KL divergence given log_probs and ref_log_probs.
396
+
397
+ Adapted from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L1150
398
+
399
+ Args:
400
+ log_probs: torch.Tensor
401
+ ref_log_probs: torch.Tensor
402
+ kl_penalty: str
403
+
404
+ Returns:
405
+ kl_div: torch.Tensor
406
+
407
+ """
408
+ log_probs, ref_log_probs = log_probs.float(), ref_log_probs.float()
409
+ if kl_penalty == "kl":
410
+ return log_probs - ref_log_probs
411
+
412
+ if kl_penalty == "abs":
413
+ return (log_probs - ref_log_probs).abs()
414
+
415
+ if kl_penalty == "mse":
416
+ return 0.5 * (log_probs - ref_log_probs).square()
417
+
418
+ # J. Schulman. Approximating kl divergence, 2020.
419
+ # URL http://joschu.net/blog/kl-approx.html
420
+ if kl_penalty == "low_var_kl":
421
+ kl = ref_log_probs - log_probs
422
+ kld = (kl.exp() - kl - 1).contiguous()
423
+ return torch.clamp(kld, min=-10, max=10)
424
+
425
+ if kl_penalty == "full":
426
+ return F.kl_div(ref_log_probs, log_probs, log_target=True, reduction="none").sum(-1)
427
+
428
+ raise NotImplementedError(f"Unknown KL penalty: {kl_penalty}.")
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/data_loader.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Optional
16
+
17
+ import torch
18
+ from torch.utils.data import RandomSampler, SequentialSampler
19
+ from torchdata.stateful_dataloader import StatefulDataLoader
20
+ from transformers import PreTrainedTokenizer, ProcessorMixin
21
+
22
+ from ..utils.dataset import RLHFDataset, collate_fn
23
+ from .config import DataConfig
24
+
25
+
26
+ def create_dataloader(config: DataConfig, tokenizer: PreTrainedTokenizer, processor: Optional[ProcessorMixin]) -> None:
27
+ train_dataset = RLHFDataset(
28
+ data_path=config.train_files,
29
+ tokenizer=tokenizer,
30
+ processor=processor,
31
+ prompt_key=config.prompt_key,
32
+ answer_key=config.answer_key,
33
+ image_key=config.image_key,
34
+ max_prompt_length=config.max_prompt_length,
35
+ truncation="right",
36
+ format_prompt=config.format_prompt,
37
+ min_pixels=config.min_pixels,
38
+ max_pixels=config.max_pixels,
39
+ filter_overlong_prompts=config.filter_overlong_prompts,
40
+ )
41
+ # use sampler for better ckpt resume
42
+ if config.shuffle:
43
+ train_dataloader_generator = torch.Generator()
44
+ train_dataloader_generator.manual_seed(config.seed)
45
+ print("train_dataset size:", len(train_dataset))
46
+ sampler = RandomSampler(data_source=train_dataset, generator=train_dataloader_generator)
47
+ else:
48
+ sampler = SequentialSampler(data_source=train_dataset)
49
+
50
+ train_dataloader = StatefulDataLoader(
51
+ dataset=train_dataset,
52
+ batch_size=config.rollout_batch_size,
53
+ sampler=sampler,
54
+ num_workers=8,
55
+ collate_fn=collate_fn,
56
+ pin_memory=False,
57
+ drop_last=True,
58
+ )
59
+
60
+ val_dataset = RLHFDataset(
61
+ data_path=config.val_files,
62
+ tokenizer=tokenizer,
63
+ processor=processor,
64
+ prompt_key=config.prompt_key,
65
+ answer_key=config.answer_key,
66
+ image_key=config.image_key,
67
+ max_prompt_length=config.max_prompt_length,
68
+ truncation="right",
69
+ format_prompt=config.format_prompt,
70
+ min_pixels=config.min_pixels,
71
+ max_pixels=config.max_pixels,
72
+ filter_overlong_prompts=config.filter_overlong_prompts,
73
+ )
74
+ val_dataloader = StatefulDataLoader(
75
+ dataset=val_dataset,
76
+ batch_size=len(val_dataset) if config.val_batch_size == -1 else config.val_batch_size,
77
+ shuffle=False,
78
+ num_workers=8,
79
+ collate_fn=collate_fn,
80
+ pin_memory=False,
81
+ drop_last=False,
82
+ )
83
+
84
+ assert len(train_dataloader) >= 1
85
+ assert len(val_dataloader) >= 1
86
+ print(f"Size of train dataloader: {len(train_dataloader)}")
87
+ print(f"Size of val dataloader: {len(val_dataloader)}")
88
+ return train_dataloader, val_dataloader
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/main.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+
17
+ import ray
18
+ from omegaconf import OmegaConf
19
+
20
+ from ..single_controller.ray import RayWorkerGroup
21
+ from ..utils.tokenizer import get_processor, get_tokenizer
22
+ from ..workers.fsdp_workers import FSDPWorker
23
+ from ..workers.reward import BatchFunctionRewardManager, SequentialFunctionRewardManager
24
+ from .config import PPOConfig
25
+ from .data_loader import create_dataloader
26
+ from .ray_trainer import RayPPOTrainer, ResourcePoolManager, Role
27
+
28
+
29
+ # please make sure main_task is not scheduled on head
30
+ @ray.remote(num_cpus=1)
31
+ class Runner:
32
+ """A runner for RL training."""
33
+
34
+ def run(self, config: PPOConfig):
35
+ # print config
36
+ print(json.dumps(config.to_dict(), indent=2))
37
+
38
+ # instantiate tokenizer
39
+ tokenizer = get_tokenizer(
40
+ config.worker.actor.model.model_path,
41
+ override_chat_template=config.data.override_chat_template,
42
+ trust_remote_code=config.worker.actor.model.trust_remote_code,
43
+ use_fast=True,
44
+ )
45
+ processor = get_processor(
46
+ config.worker.actor.model.model_path,
47
+ override_chat_template=config.data.override_chat_template,
48
+ trust_remote_code=config.worker.actor.model.trust_remote_code,
49
+ use_fast=True,
50
+ )
51
+
52
+ # define worker classes
53
+ ray_worker_group_cls = RayWorkerGroup
54
+ role_worker_mapping = {
55
+ Role.ActorRollout: ray.remote(FSDPWorker),
56
+ Role.Critic: ray.remote(FSDPWorker),
57
+ Role.RefPolicy: ray.remote(FSDPWorker),
58
+ }
59
+ global_pool_id = "global_pool"
60
+ resource_pool_spec = {
61
+ global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes,
62
+ }
63
+ mapping = {
64
+ Role.ActorRollout: global_pool_id,
65
+ Role.Critic: global_pool_id,
66
+ Role.RefPolicy: global_pool_id,
67
+ }
68
+ resource_pool_manager = ResourcePoolManager(resource_pool_spec=resource_pool_spec, mapping=mapping)
69
+
70
+ if config.worker.reward.reward_type == "sequential":
71
+ RewardManager = SequentialFunctionRewardManager
72
+ elif config.worker.reward.reward_type == "batch":
73
+ RewardManager = BatchFunctionRewardManager
74
+ else:
75
+ raise NotImplementedError(f"Unknown reward type {config.worker.reward.reward_type}.")
76
+
77
+ RemoteRewardManager = ray.remote(RewardManager).options(num_cpus=config.worker.reward.num_cpus)
78
+ reward_fn = RemoteRewardManager.remote(config.worker.reward, tokenizer)
79
+ val_reward_fn = RemoteRewardManager.remote(config.worker.reward, tokenizer)
80
+
81
+ train_dataloader, val_dataloader = create_dataloader(config.data, tokenizer, processor)
82
+
83
+ trainer = RayPPOTrainer(
84
+ config=config,
85
+ tokenizer=tokenizer,
86
+ processor=processor,
87
+ train_dataloader=train_dataloader,
88
+ val_dataloader=val_dataloader,
89
+ role_worker_mapping=role_worker_mapping,
90
+ resource_pool_manager=resource_pool_manager,
91
+ ray_worker_group_cls=ray_worker_group_cls,
92
+ reward_fn=reward_fn,
93
+ val_reward_fn=val_reward_fn,
94
+ )
95
+ trainer.init_workers()
96
+ trainer.fit()
97
+
98
+
99
+ def main():
100
+ cli_args = OmegaConf.from_cli()
101
+ default_config = OmegaConf.structured(PPOConfig())
102
+
103
+ if hasattr(cli_args, "config"):
104
+ config_path = cli_args.pop("config", None)
105
+ file_config = OmegaConf.load(config_path)
106
+ default_config = OmegaConf.merge(default_config, file_config)
107
+
108
+ ppo_config = OmegaConf.merge(default_config, cli_args)
109
+ ppo_config: PPOConfig = OmegaConf.to_object(ppo_config)
110
+ ppo_config.deep_post_init()
111
+
112
+ if not ray.is_initialized():
113
+ runtime_env = {
114
+ "env_vars": {
115
+ "TOKENIZERS_PARALLELISM": "true",
116
+ "NCCL_DEBUG": "WARN",
117
+ "VLLM_LOGGING_LEVEL": "WARN",
118
+ "TORCH_NCCL_AVOID_RECORD_STREAMS": "1",
119
+ "PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:False",
120
+ "PYTHONUNBUFFERED": "1",
121
+ }
122
+ }
123
+ ray.init(runtime_env=runtime_env)
124
+
125
+ runner = Runner.remote()
126
+ ray.get(runner.run.remote(ppo_config))
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/metrics.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Any, Dict, List
16
+
17
+ import numpy as np
18
+ import torch
19
+
20
+ from ..protocol import DataProto
21
+
22
+
23
+ def reduce_metrics(metrics: Dict[str, List[Any]]) -> Dict[str, Any]:
24
+ return {key: np.mean(value) for key, value in metrics.items()}
25
+
26
+
27
+ def compute_data_metrics(batch: DataProto, use_critic: bool = False) -> Dict[str, Any]:
28
+ sequence_score = batch.batch["token_level_scores"].sum(-1)
29
+ sequence_reward = batch.batch["token_level_rewards"].sum(-1)
30
+
31
+ advantages = batch.batch["advantages"]
32
+ returns = batch.batch["returns"]
33
+
34
+ max_response_length = batch.batch["responses"].size(-1)
35
+
36
+ prompt_mask = batch.batch["attention_mask"][:, :-max_response_length].bool()
37
+ response_mask = batch.batch["attention_mask"][:, -max_response_length:].bool()
38
+
39
+ max_prompt_length = prompt_mask.size(-1)
40
+ prompt_length = prompt_mask.sum(-1).float()
41
+ response_length = response_mask.sum(-1).float()
42
+
43
+ valid_adv = torch.masked_select(advantages, response_mask)
44
+ valid_returns = torch.masked_select(returns, response_mask)
45
+
46
+ if use_critic:
47
+ values = batch.batch["values"]
48
+ valid_values = torch.masked_select(values, response_mask)
49
+ return_diff_var = torch.var(valid_returns - valid_values)
50
+ return_var = torch.var(valid_returns)
51
+
52
+ metrics = {
53
+ # score
54
+ "critic/score/mean": torch.mean(sequence_score).detach().item(),
55
+ "critic/score/max": torch.max(sequence_score).detach().item(),
56
+ "critic/score/min": torch.min(sequence_score).detach().item(),
57
+ # reward
58
+ "critic/rewards/mean": torch.mean(sequence_reward).detach().item(),
59
+ "critic/rewards/max": torch.max(sequence_reward).detach().item(),
60
+ "critic/rewards/min": torch.min(sequence_reward).detach().item(),
61
+ # adv
62
+ "critic/advantages/mean": torch.mean(valid_adv).detach().item(),
63
+ "critic/advantages/max": torch.max(valid_adv).detach().item(),
64
+ "critic/advantages/min": torch.min(valid_adv).detach().item(),
65
+ # returns
66
+ "critic/returns/mean": torch.mean(valid_returns).detach().item(),
67
+ "critic/returns/max": torch.max(valid_returns).detach().item(),
68
+ "critic/returns/min": torch.min(valid_returns).detach().item(),
69
+ **(
70
+ {
71
+ # values
72
+ "critic/values/mean": torch.mean(valid_values).detach().item(),
73
+ "critic/values/max": torch.max(valid_values).detach().item(),
74
+ "critic/values/min": torch.min(valid_values).detach().item(),
75
+ # vf explained var
76
+ "critic/vf_explained_var": (1.0 - return_diff_var / (return_var + 1e-5)).detach().item(),
77
+ }
78
+ if use_critic
79
+ else {}
80
+ ),
81
+ # response length
82
+ "response_length/mean": torch.mean(response_length).detach().item(),
83
+ "response_length/max": torch.max(response_length).detach().item(),
84
+ "response_length/min": torch.min(response_length).detach().item(),
85
+ "response_length/clip_ratio": torch.mean(torch.eq(response_length, max_response_length).float())
86
+ .detach()
87
+ .item(),
88
+ # prompt length
89
+ "prompt_length/mean": torch.mean(prompt_length).detach().item(),
90
+ "prompt_length/max": torch.max(prompt_length).detach().item(),
91
+ "prompt_length/min": torch.min(prompt_length).detach().item(),
92
+ "prompt_length/clip_ratio": torch.mean(torch.eq(prompt_length, max_prompt_length).float()).detach().item(),
93
+ }
94
+ return metrics
95
+
96
+
97
+ def compute_timing_metrics(batch: DataProto, timing_raw: Dict[str, float]) -> Dict[str, Any]:
98
+ num_response_tokens = torch.sum(batch.batch["response_mask"]).item()
99
+ num_overall_tokens = sum(batch.meta_info["global_token_num"])
100
+ num_tokens_of_section = {
101
+ **dict.fromkeys(["gen", "reward"], num_response_tokens),
102
+ **dict.fromkeys(["ref", "old", "values", "adv", "update_critic", "update_actor"], num_overall_tokens),
103
+ }
104
+ return {
105
+ **{f"timing_s/{name}": value for name, value in timing_raw.items()},
106
+ **{
107
+ f"timing_per_token_ms/{name}": timing_raw[name] * 1000 / num_tokens_of_section[name]
108
+ for name in set(num_tokens_of_section.keys()) & set(timing_raw.keys())
109
+ },
110
+ }
111
+
112
+
113
+ def compute_throughout_metrics(batch: DataProto, timing_raw: Dict[str, float], num_gpus: int) -> Dict[str, Any]:
114
+ total_num_tokens = sum(batch.meta_info["global_token_num"])
115
+ time = timing_raw["step"]
116
+ return {
117
+ "perf/total_num_tokens": total_num_tokens,
118
+ "perf/time_per_step": time,
119
+ "perf/throughput": total_num_tokens / (time * num_gpus),
120
+ }
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/trainer/ray_trainer.py ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ FSDP PPO Trainer with Ray-based single controller.
16
+ This trainer supports model-agonistic model initialization with huggingface
17
+ """
18
+
19
+ import os
20
+ import uuid
21
+ from collections import defaultdict
22
+ from copy import deepcopy
23
+ from dataclasses import dataclass, field
24
+ from enum import Enum, IntEnum, auto
25
+ from typing import Any, Dict, List, Optional, Type
26
+
27
+ import numpy as np
28
+ import ray
29
+ import torch
30
+ from ray.experimental.tqdm_ray import tqdm
31
+ from torchdata.stateful_dataloader import StatefulDataLoader
32
+ from transformers import PreTrainedTokenizer, ProcessorMixin
33
+
34
+ from ..protocol import DataProto, pad_dataproto_to_divisor, unpad_dataproto
35
+ from ..single_controller.base import Worker
36
+ from ..single_controller.ray import RayClassWithInitArgs, RayResourcePool, RayWorkerGroup
37
+ from ..single_controller.ray.base import create_colocated_worker_cls
38
+ from ..utils import torch_functional as VF
39
+ from ..utils.checkpoint import CHECKPOINT_TRACKER, remove_obsolete_ckpt
40
+ from ..utils.logger import Tracker
41
+ from ..utils.py_functional import convert_dict_to_str, timer
42
+ from ..utils.seqlen_balancing import get_seqlen_balanced_partitions, log_seqlen_unbalance
43
+ from ..workers.fsdp_workers import FSDPWorker
44
+ from ..workers.reward import FunctionRewardManager
45
+ from . import core_algos
46
+ from .config import PPOConfig
47
+ from .metrics import compute_data_metrics, compute_throughout_metrics, compute_timing_metrics, reduce_metrics
48
+
49
+
50
+ class Role(IntEnum):
51
+ """
52
+ To create more roles dynamically, you can subclass Role and add new members
53
+ """
54
+
55
+ Actor = auto()
56
+ Rollout = auto()
57
+ ActorRollout = auto()
58
+ Critic = auto()
59
+ RefPolicy = auto()
60
+ RewardModel = auto()
61
+ ActorRolloutRef = auto()
62
+
63
+
64
+ class AdvantageEstimator(str, Enum):
65
+ """
66
+ Using an enumeration class to avoid spelling errors in adv_estimator
67
+ """
68
+
69
+ GAE = "gae"
70
+ GRPO = "grpo"
71
+ REINFORCE_PLUS_PLUS = "reinforce_plus_plus"
72
+ REMAX = "remax"
73
+ RLOO = "rloo"
74
+
75
+
76
+ @dataclass
77
+ class ResourcePoolManager:
78
+ """
79
+ Define a resource pool specification. Resource pool will be initialized first.
80
+ """
81
+
82
+ resource_pool_spec: dict[str, list[int]]
83
+ mapping: dict[Role, str]
84
+ resource_pool_dict: dict[str, RayResourcePool] = field(default_factory=dict)
85
+
86
+ def create_resource_pool(self):
87
+ for resource_pool_name, process_on_nodes in self.resource_pool_spec.items():
88
+ # max_colocate_count means the number of WorkerGroups (i.e. processes) in each RayResourcePool
89
+ # For FSDP backend, we recommend using max_colocate_count=1 that merge all WorkerGroups into one.
90
+ # For Megatron backend, we recommend using max_colocate_count>1 that can utilize different WorkerGroup for differnt models
91
+ resource_pool = RayResourcePool(
92
+ process_on_nodes=process_on_nodes, use_gpu=True, max_colocate_count=1, name_prefix=resource_pool_name
93
+ )
94
+ self.resource_pool_dict[resource_pool_name] = resource_pool
95
+
96
+ self._check_resource_available()
97
+
98
+ def get_resource_pool(self, role: Role) -> RayResourcePool:
99
+ """Get the resource pool of the worker."""
100
+ return self.resource_pool_dict[self.mapping[role]]
101
+
102
+ def get_num_gpus(self) -> int:
103
+ """Get the number of gpus in this cluster."""
104
+ return sum([n_gpus for process_on_nodes in self.resource_pool_spec.values() for n_gpus in process_on_nodes])
105
+
106
+ def _check_resource_available(self):
107
+ """Check if the resource pool can be satisfied in this ray cluster."""
108
+ gpus_available = ray.available_resources().get("GPU", 0)
109
+ gpus_required = self.get_num_gpus()
110
+ if gpus_available < gpus_required:
111
+ raise ValueError(f"Total available GPUs {gpus_available} is less than total desired GPUs {gpus_required}.")
112
+
113
+
114
+ def apply_kl_penalty(data: DataProto, kl_ctrl: core_algos.KLController, kl_penalty="kl"):
115
+ token_level_scores = data.batch["token_level_scores"]
116
+ batch_size = data.batch.batch_size[0]
117
+ response_mask = data.batch["response_mask"]
118
+
119
+ # compute kl between ref_policy and current policy
120
+ kld = core_algos.compute_kl(data.batch["old_log_probs"], data.batch["ref_log_probs"], kl_penalty=kl_penalty)
121
+ kld = kld * response_mask # (batch_size, response_length)
122
+
123
+ data.batch["token_level_rewards"] = token_level_scores - kl_ctrl.kl_coef * kld
124
+
125
+ current_kl = VF.masked_mean(kld, mask=response_mask, dim=-1) # average over sequence
126
+ current_kl = torch.mean(current_kl, dim=0).item()
127
+ metrics = {"critic/kl": current_kl, "critic/kl_coef": kl_ctrl.kl_coef}
128
+
129
+ # According to https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L880
130
+ kl_ctrl.update(current_kl=current_kl, n_steps=batch_size)
131
+ return data, metrics
132
+
133
+
134
+ def compute_advantage(data: DataProto, adv_estimator: AdvantageEstimator, gamma: float = 1.0, lam: float = 1.0):
135
+ token_level_rewards = data.batch["token_level_rewards"]
136
+ response_mask = data.batch["response_mask"]
137
+ index = data.non_tensor_batch["uid"]
138
+ if adv_estimator == AdvantageEstimator.GAE:
139
+ values = data.batch["values"]
140
+ advantages, returns = core_algos.compute_gae_advantage_return(
141
+ token_level_rewards, values, response_mask, gamma, lam
142
+ )
143
+ elif adv_estimator == AdvantageEstimator.GRPO:
144
+ advantages, returns = core_algos.compute_grpo_outcome_advantage(token_level_rewards, response_mask, index)
145
+ elif adv_estimator == AdvantageEstimator.REINFORCE_PLUS_PLUS:
146
+ advantages, returns = core_algos.compute_reinforce_plus_plus_outcome_advantage(
147
+ token_level_rewards, response_mask, gamma
148
+ )
149
+ elif adv_estimator == AdvantageEstimator.REMAX:
150
+ reward_baselines = data.batch["reward_baselines"]
151
+ advantages, returns = core_algos.compute_remax_outcome_advantage(
152
+ token_level_rewards, reward_baselines, response_mask
153
+ )
154
+ elif adv_estimator == AdvantageEstimator.RLOO:
155
+ advantages, returns = core_algos.compute_rloo_outcome_advantage(token_level_rewards, response_mask, index)
156
+ else:
157
+ raise NotImplementedError
158
+
159
+ data.batch["advantages"] = advantages
160
+ data.batch["returns"] = returns
161
+ return data
162
+
163
+
164
+ class RayPPOTrainer:
165
+ """
166
+ Note that this trainer runs on the driver process on a single CPU/GPU node.
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ config: PPOConfig,
172
+ tokenizer: PreTrainedTokenizer,
173
+ processor: Optional[ProcessorMixin],
174
+ train_dataloader: StatefulDataLoader,
175
+ val_dataloader: StatefulDataLoader,
176
+ role_worker_mapping: dict[Role, Type[Worker]],
177
+ resource_pool_manager: ResourcePoolManager,
178
+ ray_worker_group_cls: Type[RayWorkerGroup] = RayWorkerGroup,
179
+ reward_fn: Optional[FunctionRewardManager] = None,
180
+ val_reward_fn: Optional[FunctionRewardManager] = None,
181
+ ):
182
+ self.tokenizer = tokenizer
183
+ self.processor = processor
184
+ self.train_dataloader = train_dataloader
185
+ self.val_dataloader = val_dataloader
186
+ self.config = config
187
+ self.reward_fn = reward_fn
188
+ self.val_reward_fn = val_reward_fn
189
+
190
+ self.hybrid_engine = config.worker.hybrid_engine
191
+ if self.hybrid_engine:
192
+ assert Role.ActorRollout in role_worker_mapping, (
193
+ f"ActorRollout should be included in {role_worker_mapping.keys()}."
194
+ )
195
+ else:
196
+ raise NotImplementedError
197
+
198
+ self.role_worker_mapping = role_worker_mapping
199
+ self.resource_pool_manager = resource_pool_manager
200
+ self.use_reward_model = Role.RewardModel in role_worker_mapping
201
+ self.ray_worker_group_cls = ray_worker_group_cls
202
+
203
+ # define KL control
204
+ if Role.RefPolicy in role_worker_mapping and not config.algorithm.disable_kl:
205
+ self.use_reference_policy = True
206
+ self.kl_ctrl = core_algos.get_kl_controller(config.algorithm)
207
+ else:
208
+ self.use_reference_policy = False
209
+ self.kl_ctrl = core_algos.FixedKLController(init_kl_coef=0.0)
210
+ print("KL is disabled, no KL metrics will be logged. Please set `kl_coef=0` to log KL metrics.")
211
+
212
+ if config.algorithm.adv_estimator == AdvantageEstimator.GAE:
213
+ self.use_critic = True
214
+ else:
215
+ self.use_critic = False
216
+
217
+ if config.algorithm.adv_estimator not in list(AdvantageEstimator):
218
+ raise NotImplementedError(f"Unknown advantage estimator: {config.algorithm.adv_estimator}.")
219
+
220
+ if config.data.rollout_batch_size % config.worker.actor.global_batch_size != 0:
221
+ raise ValueError("Rollout batch size must be divisible by actor global batch size.")
222
+
223
+ if (
224
+ config.data.rollout_batch_size * config.worker.rollout.n
225
+ ) % config.worker.actor.micro_batch_size_per_device_for_experience != 0:
226
+ raise ValueError(
227
+ "Rollout batch size * rollout.n must be divisible by actor micro batch size for experience."
228
+ )
229
+
230
+ if self.use_critic:
231
+ if config.data.rollout_batch_size % config.worker.critic.global_batch_size != 0:
232
+ raise ValueError("Rollout batch size must be divisible by critic global batch size.")
233
+
234
+ if (
235
+ config.data.rollout_batch_size * config.worker.rollout.n
236
+ ) % config.worker.critic.micro_batch_size_per_device_for_experience != 0:
237
+ raise ValueError(
238
+ "Rollout batch size * rollout.n must be divisible by critic micro batch size for experience."
239
+ )
240
+
241
+ if (
242
+ config.algorithm.adv_estimator in (AdvantageEstimator.GRPO, AdvantageEstimator.RLOO)
243
+ and config.worker.rollout.n == 1
244
+ ):
245
+ raise ValueError("GRPO and RLOO algorithm need `config.worker.rollout.n > 1`.")
246
+
247
+ if config.trainer.max_steps is not None:
248
+ self.training_steps = config.trainer.max_steps
249
+ else:
250
+ self.training_steps = len(train_dataloader) * config.trainer.total_epochs
251
+
252
+ config.worker.actor.optim.training_steps = self.training_steps
253
+ config.worker.critic.optim.training_steps = self.training_steps
254
+ print(f"Total training steps: {self.training_steps}")
255
+
256
+ def _maybe_log_val_generations(
257
+ self, inputs: List[str], outputs: List[str], labels: List[str], scores: List[float]
258
+ ) -> None:
259
+ """Log a table of validation samples"""
260
+ if self.config.trainer.val_generations_to_log <= 0:
261
+ return
262
+
263
+ # Create tuples of (input, output, score) and sort by input text
264
+ samples = list(zip(inputs, outputs, labels, scores))
265
+ samples.sort(key=lambda x: x[0]) # Sort by input text
266
+
267
+ # Use fixed random seed for deterministic shuffling
268
+ rng = np.random.RandomState(42)
269
+ rng.shuffle(samples)
270
+
271
+ samples = samples[: self.config.trainer.val_generations_to_log]
272
+ self.logger.log_generation(samples, self.global_step)
273
+
274
+ def _validate(self) -> Dict[str, Any]:
275
+ reward_tensor_lst = []
276
+ # Lists to collect samples for the table
277
+ sample_inputs, sample_outputs, sample_labels, sample_scores = [], [], [], []
278
+ reward_metrics_lst = defaultdict(list)
279
+ for batch_dict in self.val_dataloader:
280
+ test_batch = DataProto.from_single_dict(batch_dict)
281
+ # Store original inputs
282
+ input_ids = test_batch.batch["input_ids"]
283
+ input_texts = [self.tokenizer.decode(ids, skip_special_tokens=True) for ids in input_ids]
284
+ sample_inputs.extend(input_texts)
285
+
286
+ if "multi_modal_data" in test_batch.non_tensor_batch.keys():
287
+ test_gen_batch = test_batch.pop(
288
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
289
+ non_tensor_batch_keys=["raw_prompt_ids", "multi_modal_data"],
290
+ )
291
+ else:
292
+ test_gen_batch = test_batch.pop(
293
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
294
+ non_tensor_batch_keys=["raw_prompt_ids"],
295
+ )
296
+
297
+ test_gen_batch.meta_info = self.config.worker.rollout.val_override_config
298
+ test_gen_batch.meta_info.update({
299
+ "min_pixels": self.config.data.min_pixels,
300
+ "max_pixels": self.config.data.max_pixels,
301
+ })
302
+ test_gen_batch, pad_size = pad_dataproto_to_divisor(test_gen_batch, self.actor_rollout_wg.world_size)
303
+ test_output_gen_batch = self.actor_rollout_wg.generate_sequences(test_gen_batch)
304
+ test_output_gen_batch = unpad_dataproto(test_output_gen_batch, pad_size=pad_size)
305
+
306
+ # Store generated outputs
307
+ output_ids = test_output_gen_batch.batch["responses"]
308
+ output_texts = [self.tokenizer.decode(ids, skip_special_tokens=True) for ids in output_ids]
309
+ sample_outputs.extend(output_texts)
310
+ sample_labels.extend(test_batch.non_tensor_batch["ground_truth"].tolist())
311
+ test_batch = test_batch.union(test_output_gen_batch)
312
+
313
+ # evaluate using reward_function
314
+ reward_tensor, reward_metrics = ray.get(self.val_reward_fn.compute_reward.remote(test_batch))
315
+
316
+ # Store scores
317
+ scores = reward_tensor.sum(-1).cpu().tolist()
318
+ sample_scores.extend(scores)
319
+
320
+ reward_tensor_lst.append(reward_tensor)
321
+ for key, value in reward_metrics.items():
322
+ reward_metrics_lst[key].extend(value)
323
+
324
+ self._maybe_log_val_generations(sample_inputs, sample_outputs, sample_labels, sample_scores)
325
+ reward_score = torch.cat(reward_tensor_lst, dim=0).sum(-1).mean().item()
326
+ val_reward_metrics = {f"val/{key}_reward": value for key, value in reduce_metrics(reward_metrics_lst).items()}
327
+ return {"val/reward_score": reward_score, **val_reward_metrics}
328
+
329
+ def init_workers(self) -> None:
330
+ """Init resource pool and worker group"""
331
+ self.resource_pool_manager.create_resource_pool()
332
+ self.resource_pool_to_cls = {pool: {} for pool in self.resource_pool_manager.resource_pool_dict.values()}
333
+
334
+ # create actor and rollout
335
+ if self.hybrid_engine:
336
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.ActorRollout)
337
+ actor_rollout_cls = RayClassWithInitArgs(
338
+ cls=self.role_worker_mapping[Role.ActorRollout], config=self.config.worker, role="actor_rollout"
339
+ )
340
+ self.resource_pool_to_cls[resource_pool]["actor_rollout"] = actor_rollout_cls
341
+ else:
342
+ raise NotImplementedError
343
+
344
+ # create critic
345
+ if self.use_critic:
346
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.Critic)
347
+ critic_cls = RayClassWithInitArgs(
348
+ cls=self.role_worker_mapping[Role.Critic], config=self.config.worker, role="critic"
349
+ )
350
+ self.resource_pool_to_cls[resource_pool]["critic"] = critic_cls
351
+
352
+ # create reference policy if needed
353
+ if self.use_reference_policy:
354
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.RefPolicy)
355
+ ref_policy_cls = RayClassWithInitArgs(
356
+ self.role_worker_mapping[Role.RefPolicy], config=self.config.worker, role="ref"
357
+ )
358
+ self.resource_pool_to_cls[resource_pool]["ref"] = ref_policy_cls
359
+
360
+ # create a reward model if reward_fn is None
361
+ if self.use_reward_model:
362
+ # we create a RM here
363
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.RewardModel)
364
+ rm_cls = RayClassWithInitArgs(
365
+ cls=self.role_worker_mapping[Role.RewardModel], config=self.config.worker, role="reward"
366
+ )
367
+ self.resource_pool_to_cls[resource_pool]["rm"] = rm_cls
368
+
369
+ # initialize WorkerGroup
370
+ # NOTE: if you want to use a different resource pool for each role, which can support different parallel size,
371
+ # you should not use `create_colocated_worker_cls`. Instead, directly pass different resource pool to different worker groups.
372
+ # See https://github.com/volcengine/verl/blob/master/examples/ray/tutorial.ipynb for more information.
373
+ all_wg: Dict[str, FSDPWorker] = {}
374
+ self.wg_dicts = []
375
+ for resource_pool, class_dict in self.resource_pool_to_cls.items():
376
+ worker_dict_cls = create_colocated_worker_cls(class_dict=class_dict)
377
+ wg_dict = self.ray_worker_group_cls(resource_pool=resource_pool, ray_cls_with_init=worker_dict_cls)
378
+ spawn_wg = wg_dict.spawn(prefix_set=class_dict.keys())
379
+ all_wg.update(spawn_wg)
380
+ # keep the referece of WorkerDict to support ray >= 2.31. Ref: https://github.com/ray-project/ray/pull/45699
381
+ self.wg_dicts.append(wg_dict)
382
+
383
+ if self.use_critic:
384
+ self.critic_wg = all_wg["critic"]
385
+ self.critic_wg.init_model()
386
+
387
+ if self.use_reference_policy:
388
+ self.ref_policy_wg = all_wg["ref"]
389
+ self.ref_policy_wg.init_model()
390
+
391
+ if self.use_reward_model:
392
+ self.rm_wg = all_wg["rm"]
393
+ self.rm_wg.init_model()
394
+
395
+ # we should create rollout at the end so that vllm can have a better estimation of kv cache memory
396
+ self.actor_rollout_wg = all_wg["actor_rollout"]
397
+ self.actor_rollout_wg.init_model()
398
+
399
+ def _save_checkpoint(self) -> None:
400
+ # path: {save_checkpoint_path}/global_step_{global_step}/{actor,critic}
401
+ remove_obsolete_ckpt(
402
+ self.config.trainer.save_checkpoint_path, self.global_step, self.config.trainer.save_limit
403
+ )
404
+ folder_path = os.path.join(self.config.trainer.save_checkpoint_path, f"global_step_{self.global_step}")
405
+ actor_path = os.path.join(folder_path, "actor")
406
+ self.actor_rollout_wg.save_checkpoint(actor_path)
407
+
408
+ if self.use_critic:
409
+ critic_path = os.path.join(folder_path, "critic")
410
+ self.critic_wg.save_checkpoint(critic_path)
411
+
412
+ dataloader_path = os.path.join(folder_path, "dataloader.pt")
413
+ dataloader_state_dict = self.train_dataloader.state_dict()
414
+ torch.save(dataloader_state_dict, dataloader_path)
415
+
416
+ last_global_step_path = os.path.join(self.config.trainer.save_checkpoint_path, CHECKPOINT_TRACKER)
417
+ with open(last_global_step_path, "w") as f:
418
+ f.write(str(self.global_step))
419
+
420
+ def _load_checkpoint(self) -> None:
421
+ if self.config.trainer.load_checkpoint_path is None:
422
+ return
423
+
424
+ if "global_step_" not in self.config.trainer.load_checkpoint_path.strip(os.path.sep).split(os.path.sep)[-1]:
425
+ raise ValueError("`load_checkpoint_path` should end with `global_step_*`.")
426
+
427
+ print(f"Load from checkpoint: {self.config.trainer.load_checkpoint_path}.")
428
+ self.global_step = int(self.config.trainer.load_checkpoint_path.strip(os.path.sep).split("global_step_")[-1])
429
+ actor_path = os.path.join(self.config.trainer.load_checkpoint_path, "actor")
430
+ self.actor_rollout_wg.load_checkpoint(actor_path)
431
+ if self.use_critic:
432
+ critic_path = os.path.join(self.config.trainer.load_checkpoint_path, "critic")
433
+ self.critic_wg.load_checkpoint(critic_path)
434
+
435
+ dataloader_path = os.path.join(self.config.trainer.load_checkpoint_path, "dataloader.pt")
436
+ if os.path.exists(dataloader_path):
437
+ dataloader_state_dict = torch.load(dataloader_path, weights_only=False)
438
+ self.train_dataloader.load_state_dict(dataloader_state_dict)
439
+ else:
440
+ print(f"No dataloader state found at {dataloader_path}, will start from scratch.")
441
+
442
+ def _balance_batch(self, batch: DataProto, metrics: Dict[str, Any], logging_prefix: str = "global_seqlen") -> None:
443
+ """Reorder the data on single controller such that each dp rank gets similar total tokens"""
444
+ attention_mask = batch.batch["attention_mask"]
445
+ batch_size = attention_mask.shape[0]
446
+ global_seqlen_lst = batch.batch["attention_mask"].view(batch_size, -1).sum(-1).tolist() # (train_batch_size,)
447
+ world_size = self.actor_rollout_wg.world_size
448
+ global_partition_lst = get_seqlen_balanced_partitions(
449
+ global_seqlen_lst, k_partitions=world_size, equal_size=True
450
+ )
451
+ # reorder based on index. The data will be automatically equally partitioned by dispatch function
452
+ global_idx = torch.tensor([j for partition in global_partition_lst for j in partition])
453
+ batch.reorder(global_idx)
454
+ global_balance_stats = log_seqlen_unbalance(
455
+ seqlen_list=global_seqlen_lst, partitions=global_partition_lst, prefix=logging_prefix
456
+ )
457
+ metrics.update(global_balance_stats)
458
+
459
+ def fit(self):
460
+ """
461
+ The training loop of PPO.
462
+ The driver process only need to call the compute functions of the worker group through RPC to construct the PPO dataflow.
463
+ The light-weight advantage computation is done on the driver process.
464
+ """
465
+ self.logger = Tracker(loggers=self.config.trainer.logger, config=self.config.to_dict())
466
+ self.global_step = 0
467
+ val_metrics: Optional[Dict[str, Any]] = None
468
+
469
+ # load checkpoint before doing anything
470
+ self._load_checkpoint()
471
+
472
+ # perform validation before training
473
+ # currently, we only support validation using the reward_function.
474
+ if self.val_reward_fn is not None and self.config.trainer.val_before_train:
475
+ val_metrics = self._validate()
476
+ self.logger.log(data=val_metrics, step=self.global_step)
477
+ if self.config.trainer.val_only:
478
+ return
479
+
480
+ for _ in tqdm(range(self.config.trainer.total_epochs), desc="Epoch", position=0):
481
+ for batch_dict in tqdm(self.train_dataloader, desc="Running step", position=1):
482
+ self.global_step += 1
483
+ if self.global_step > self.training_steps:
484
+ break
485
+
486
+ metrics, timing_raw = {}, {}
487
+ batch: DataProto = DataProto.from_single_dict(batch_dict)
488
+
489
+ # pop those keys for generation
490
+ if "multi_modal_data" in batch.non_tensor_batch.keys():
491
+ print(batch.non_tensor_batch['multi_modal_data'].shape)
492
+ gen_batch = batch.pop(
493
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
494
+ non_tensor_batch_keys=["raw_prompt_ids", "multi_modal_data"],
495
+ )
496
+ gen_batch.meta_info.update({
497
+ "min_pixels": self.config.data.min_pixels,
498
+ "max_pixels": self.config.data.max_pixels,
499
+ })
500
+ else:
501
+ gen_batch = batch.pop(
502
+ batch_keys=["input_ids", "attention_mask", "position_ids"],
503
+ non_tensor_batch_keys=["raw_prompt_ids"],
504
+ )
505
+
506
+ with timer("step", timing_raw):
507
+ # generate a batch
508
+ with timer("gen", timing_raw): # wg: worker group
509
+ gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch)
510
+
511
+ if self.config.algorithm.adv_estimator == "remax":
512
+ with timer("gen_max", timing_raw):
513
+ gen_baseline_batch = deepcopy(gen_batch)
514
+ gen_baseline_batch.meta_info["temperature"] = 0
515
+ gen_baseline_batch.meta_info["n"] = 1
516
+ gen_baseline_output = self.actor_rollout_wg.generate_sequences(gen_baseline_batch)
517
+
518
+ batch = batch.union(gen_baseline_output)
519
+ reward_baseline_tensor, _ = ray.get(self.reward_fn.compute_reward.remote(batch))
520
+ reward_baseline_tensor = reward_baseline_tensor.sum(dim=-1)
521
+
522
+ batch.pop(batch_keys=list(gen_baseline_output.batch.keys()))
523
+ batch.batch["reward_baselines"] = reward_baseline_tensor
524
+ del gen_baseline_batch, gen_baseline_output
525
+
526
+ batch.non_tensor_batch["uid"] = np.array(
527
+ [str(uuid.uuid4()) for _ in range(len(batch.batch))], dtype=object
528
+ )
529
+ # repeat to align with repeated responses in rollout
530
+ batch = batch.repeat(repeat_times=self.config.worker.rollout.n, interleave=True)
531
+ batch = batch.union(gen_batch_output)
532
+
533
+ # balance the number of valid tokens on each dp rank.
534
+ # Note that this breaks the order of data inside the batch.
535
+ # Please take care when you implement group based adv computation such as GRPO and rloo
536
+ self._balance_batch(batch, metrics=metrics)
537
+
538
+ # compute global_valid tokens
539
+ batch.meta_info["global_token_num"] = torch.sum(batch.batch["attention_mask"], dim=-1).tolist()
540
+
541
+ # compute reward
542
+ with timer("reward", timing_raw):
543
+ reward_ref = self.reward_fn.compute_reward.remote(batch)
544
+
545
+ # recompute old_log_probs
546
+ with timer("old", timing_raw):
547
+ old_log_probs = self.actor_rollout_wg.compute_log_probs(batch)
548
+ batch = batch.union(old_log_probs)
549
+
550
+ # compute ref_log_probs
551
+ if self.use_reference_policy:
552
+ with timer("ref", timing_raw):
553
+ ref_log_probs = self.ref_policy_wg.compute_ref_log_probs(batch)
554
+ batch = batch.union(ref_log_probs)
555
+
556
+ # compute values
557
+ if self.use_critic:
558
+ with timer("values", timing_raw):
559
+ values = self.critic_wg.compute_values(batch)
560
+ batch = batch.union(values)
561
+
562
+ with timer("adv", timing_raw):
563
+ # get token level scores
564
+ reward_tensor, reward_metrics = ray.get(reward_ref)
565
+ batch.batch["token_level_scores"] = reward_tensor
566
+ reward_metrics = {f"reward/{k}": v for k, v in reduce_metrics(reward_metrics).items()}
567
+ metrics.update(reward_metrics)
568
+
569
+ # apply kl penalty if available
570
+ if not self.config.algorithm.use_kl_loss and self.use_reference_policy:
571
+ # apply kl penalty to reward
572
+ batch, kl_metrics = apply_kl_penalty(batch, self.kl_ctrl, self.config.algorithm.kl_penalty)
573
+ metrics.update(kl_metrics)
574
+ else:
575
+ batch.batch["token_level_rewards"] = batch.batch["token_level_scores"]
576
+
577
+ # compute advantages, executed on the driver process
578
+ batch = compute_advantage(
579
+ batch,
580
+ adv_estimator=self.config.algorithm.adv_estimator,
581
+ gamma=self.config.algorithm.gamma,
582
+ lam=self.config.algorithm.lam,
583
+ )
584
+
585
+ # update critic
586
+ if self.use_critic:
587
+ with timer("update_critic", timing_raw):
588
+ critic_output = self.critic_wg.update_critic(batch)
589
+
590
+ critic_metrics = reduce_metrics(critic_output.non_tensor_batch)
591
+ metrics.update(critic_metrics)
592
+
593
+ # update actor
594
+ if self.config.trainer.critic_warmup <= self.global_step:
595
+ with timer("update_actor", timing_raw):
596
+ actor_output = self.actor_rollout_wg.update_actor(batch)
597
+
598
+ actor_metrics = reduce_metrics(actor_output.non_tensor_batch)
599
+ metrics.update(actor_metrics)
600
+
601
+ # validate
602
+ if (
603
+ self.val_reward_fn is not None
604
+ and self.config.trainer.val_freq > 0
605
+ and self.global_step % self.config.trainer.val_freq == 0
606
+ ):
607
+ with timer("validation", timing_raw):
608
+ val_metrics = self._validate()
609
+
610
+ metrics.update(val_metrics)
611
+
612
+ if self.config.trainer.save_freq > 0 and self.global_step % self.config.trainer.save_freq == 0:
613
+ with timer("save_checkpoint", timing_raw):
614
+ self._save_checkpoint()
615
+
616
+ # collect metrics
617
+ num_gpus = self.resource_pool_manager.get_num_gpus()
618
+ metrics.update(compute_data_metrics(batch=batch, use_critic=self.use_critic))
619
+ metrics.update(compute_timing_metrics(batch=batch, timing_raw=timing_raw))
620
+ metrics.update(compute_throughout_metrics(batch=batch, timing_raw=timing_raw, num_gpus=num_gpus))
621
+
622
+ self.logger.log(data=metrics, step=self.global_step)
623
+
624
+ # perform validation after training
625
+ if self.val_reward_fn is not None:
626
+ if (
627
+ val_metrics is None
628
+ or self.config.trainer.val_freq <= 0
629
+ or self.global_step % self.config.trainer.val_freq != 0
630
+ ):
631
+ val_metrics = self._validate()
632
+ self.logger.log(data=val_metrics, step=self.global_step)
633
+
634
+ print(f"Final validation metrics: {convert_dict_to_str(val_metrics)}")
635
+
636
+ if self.config.trainer.save_freq <= 0 or self.global_step % self.config.trainer.save_freq != 0:
637
+ self._save_checkpoint()
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .checkpoint_manager import CHECKPOINT_TRACKER, remove_obsolete_ckpt
16
+
17
+
18
+ __all__ = ["CHECKPOINT_TRACKER", "remove_obsolete_ckpt"]
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/checkpoint_manager.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import random
17
+ import re
18
+ import shutil
19
+ import tempfile
20
+ from abc import ABC, abstractmethod
21
+ from typing import Any, Dict, Optional, Union
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.distributed as dist
26
+ from filelock import FileLock
27
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
28
+ from transformers import PreTrainedTokenizer, ProcessorMixin
29
+
30
+
31
+ CHECKPOINT_TRACKER = "latest_global_step.txt"
32
+
33
+
34
+ class BaseCheckpointManager(ABC):
35
+ """
36
+ A checkpoint manager that saves and loads
37
+ - model
38
+ - optimizer
39
+ - lr_scheduler
40
+ - extra_states
41
+ in a SPMD way.
42
+
43
+ We save
44
+ - sharded model states and optimizer states
45
+ - full lr_scheduler states
46
+ - huggingface tokenizer and config for ckpt merge
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ model: FSDP,
52
+ optimizer: torch.optim.Optimizer,
53
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler,
54
+ processing_class: Union[PreTrainedTokenizer, ProcessorMixin],
55
+ ):
56
+ self.model = model
57
+ self.optimizer = optimizer
58
+ self.lr_scheduler = lr_scheduler
59
+ self.processing_class = processing_class
60
+
61
+ assert isinstance(self.model, FSDP)
62
+ self.rank = dist.get_rank()
63
+ self.world_size = dist.get_world_size()
64
+
65
+ @abstractmethod
66
+ def load_checkpoint(self, *args, **kwargs):
67
+ raise NotImplementedError
68
+
69
+ @abstractmethod
70
+ def save_checkpoint(self, *args, **kwargs):
71
+ raise NotImplementedError
72
+
73
+ @staticmethod
74
+ def local_mkdir(path: str) -> str:
75
+ if not os.path.isabs(path):
76
+ working_dir = os.getcwd()
77
+ path = os.path.join(working_dir, path)
78
+
79
+ # Using hash value of path as lock file name to avoid long file name
80
+ lock_filename = f"ckpt_{hash(path) & 0xFFFFFFFF:08x}.lock"
81
+ lock_path = os.path.join(tempfile.gettempdir(), lock_filename)
82
+
83
+ try:
84
+ with FileLock(lock_path, timeout=60):
85
+ os.makedirs(path, exist_ok=True)
86
+ except Exception as e:
87
+ print(f"Warning: Failed to acquire lock for {path}: {e}")
88
+ os.makedirs(path, exist_ok=True) # even if the lock is not acquired, try to create the directory
89
+
90
+ return path
91
+
92
+ @staticmethod
93
+ def get_rng_state() -> Dict[str, Any]:
94
+ rng_state = {
95
+ "cpu": torch.get_rng_state(),
96
+ "cuda": torch.cuda.get_rng_state(),
97
+ "numpy": np.random.get_state(),
98
+ "random": random.getstate(),
99
+ }
100
+ return rng_state
101
+
102
+ @staticmethod
103
+ def load_rng_state(rng_state: Dict[str, Any]):
104
+ torch.set_rng_state(rng_state["cpu"])
105
+ torch.cuda.set_rng_state(rng_state["cuda"])
106
+ np.random.set_state(rng_state["numpy"])
107
+ random.setstate(rng_state["random"])
108
+
109
+
110
+ def find_latest_ckpt_path(path: Optional[str] = None, directory_format: str = "global_step_{}") -> Optional[str]:
111
+ if path is None:
112
+ return None
113
+
114
+ tracker_file = get_checkpoint_tracker_filename(path)
115
+ if not os.path.exists(tracker_file):
116
+ print("Checkpoint tracker file does not exist: %s", tracker_file)
117
+ return None
118
+
119
+ with open(tracker_file, "rb") as f:
120
+ iteration = int(f.read().decode())
121
+
122
+ ckpt_path = os.path.join(path, directory_format.format(iteration))
123
+ if not os.path.exists(ckpt_path):
124
+ print("Checkpoint does not exist: %s", ckpt_path)
125
+ return None
126
+
127
+ print("Found checkpoint: %s", ckpt_path)
128
+ return ckpt_path
129
+
130
+
131
+ def get_checkpoint_tracker_filename(root_path: str) -> str:
132
+ """
133
+ Tracker file rescords the latest chckpoint during training to restart from.
134
+ """
135
+ return os.path.join(root_path, CHECKPOINT_TRACKER)
136
+
137
+
138
+ def remove_obsolete_ckpt(path: str, global_step: int, save_limit: int = -1, directory_format: str = "global_step_{}"):
139
+ """
140
+ Remove the obsolete checkpoints that exceed the save_limit.
141
+ """
142
+ if save_limit <= 0:
143
+ return
144
+
145
+ if not os.path.exists(path):
146
+ return
147
+
148
+ pattern = re.escape(directory_format).replace(r"\{\}", r"(\d+)")
149
+ ckpt_folders = []
150
+ for folder in os.listdir(path):
151
+ if match := re.match(pattern, folder):
152
+ step = int(match.group(1))
153
+ if step < global_step:
154
+ ckpt_folders.append((step, folder))
155
+
156
+ ckpt_folders.sort(reverse=True)
157
+ for _, folder in ckpt_folders[save_limit - 1 :]:
158
+ folder_path = os.path.join(path, folder)
159
+ shutil.rmtree(folder_path, ignore_errors=True)
160
+ print(f"Removed obsolete checkpoint: {folder_path}")
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/checkpoint/fsdp_checkpoint_manager.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ from typing import Optional, Union
17
+
18
+ import torch
19
+ import torch.distributed as dist
20
+ from torch.distributed.checkpoint.state_dict import StateDictOptions, get_state_dict, set_state_dict
21
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
22
+ from transformers import PreTrainedModel, PreTrainedTokenizer, ProcessorMixin
23
+
24
+ from .checkpoint_manager import BaseCheckpointManager
25
+
26
+
27
+ class FSDPCheckpointManager(BaseCheckpointManager):
28
+ """
29
+ A checkpoint manager that saves and loads
30
+ - model
31
+ - optimizer
32
+ - lr_scheduler
33
+ - extra_states
34
+ in a SPMD way.
35
+
36
+ We save
37
+ - sharded model states and optimizer states
38
+ - full lr_scheduler states
39
+ - huggingface tokenizer and config for ckpt merge
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ model: FSDP,
45
+ optimizer: torch.optim.Optimizer,
46
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler,
47
+ processing_class: Union[PreTrainedTokenizer, ProcessorMixin],
48
+ ):
49
+ super().__init__(model, optimizer, lr_scheduler, processing_class)
50
+
51
+ def load_checkpoint(self, path: Optional[str] = None):
52
+ if path is None:
53
+ return
54
+
55
+ # every rank download its own checkpoint
56
+ model_path = os.path.join(path, f"model_world_size_{self.world_size}_rank_{self.rank}.pt")
57
+ optim_path = os.path.join(path, f"optim_world_size_{self.world_size}_rank_{self.rank}.pt")
58
+ extra_path = os.path.join(path, f"extra_state_world_size_{self.world_size}_rank_{self.rank}.pt")
59
+ print(f"[rank-{self.rank}]: Loading model from {os.path.abspath(model_path)}.")
60
+ print(f"[rank-{self.rank}]: Loading optimizer from {os.path.abspath(optim_path)}.")
61
+ print(f"[rank-{self.rank}]: Loading extra_state from {os.path.abspath(extra_path)}.")
62
+ model_state_dict = torch.load(model_path, weights_only=False)
63
+ optim_state_dict = torch.load(optim_path, weights_only=False)
64
+ extra_state_dict = torch.load(extra_path, weights_only=False)
65
+
66
+ state_dict_options = StateDictOptions(cpu_offload=True)
67
+ set_state_dict(
68
+ model=self.model,
69
+ optimizers=self.optimizer,
70
+ model_state_dict=model_state_dict,
71
+ optim_state_dict=optim_state_dict,
72
+ options=state_dict_options,
73
+ )
74
+ self.lr_scheduler.load_state_dict(extra_state_dict["lr_scheduler"])
75
+
76
+ # recover random state
77
+ if "rng" in extra_state_dict:
78
+ self.load_rng_state(extra_state_dict["rng"])
79
+
80
+ def save_checkpoint(self, path: str):
81
+ path = self.local_mkdir(path)
82
+ dist.barrier()
83
+
84
+ # every rank will save its own model and optim shard
85
+ state_dict_options = StateDictOptions(cpu_offload=True)
86
+ model_state_dict, optim_state_dict = get_state_dict(self.model, self.optimizer, options=state_dict_options)
87
+ extra_state_dict = {
88
+ "lr_scheduler": self.lr_scheduler.state_dict(),
89
+ "rng": self.get_rng_state(),
90
+ }
91
+ model_path = os.path.join(path, f"model_world_size_{self.world_size}_rank_{self.rank}.pt")
92
+ optim_path = os.path.join(path, f"optim_world_size_{self.world_size}_rank_{self.rank}.pt")
93
+ extra_path = os.path.join(path, f"extra_state_world_size_{self.world_size}_rank_{self.rank}.pt")
94
+
95
+ print(f"[rank-{self.rank}]: Saving model to {os.path.abspath(model_path)}.")
96
+ print(f"[rank-{self.rank}]: Saving optimizer to {os.path.abspath(optim_path)}.")
97
+ print(f"[rank-{self.rank}]: Saving extra_state to {os.path.abspath(extra_path)}.")
98
+ torch.save(model_state_dict, model_path)
99
+ torch.save(optim_state_dict, optim_path)
100
+ torch.save(extra_state_dict, extra_path)
101
+
102
+ # wait for everyone to dump to local
103
+ dist.barrier()
104
+
105
+ if self.rank == 0:
106
+ hf_path = os.path.join(path, "huggingface")
107
+ os.makedirs(hf_path, exist_ok=True)
108
+ assert isinstance(self.model._fsdp_wrapped_module, PreTrainedModel)
109
+ self.model._fsdp_wrapped_module.config.save_pretrained(hf_path)
110
+ self.model._fsdp_wrapped_module.generation_config.save_pretrained(hf_path)
111
+ self.processing_class.save_pretrained(hf_path)
112
+
113
+ dist.barrier()
LLaVA-NeXT/llava-critic-r1/EasyR1/verl/utils/dataset.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import math
16
+ import os
17
+ from collections import defaultdict
18
+ from io import BytesIO
19
+ from typing import Any, Dict, List, Optional, Union
20
+
21
+ import numpy as np
22
+ import torch
23
+ from datasets import load_dataset
24
+ from jinja2 import Template
25
+ from PIL import Image
26
+ from PIL.Image import Image as ImageObject
27
+ from PIL.Image import Image as PILImage
28
+ from torch.utils.data import Dataset
29
+ from transformers import PreTrainedTokenizer, ProcessorMixin
30
+ from PIL import Image, JpegImagePlugin
31
+
32
+ from ..models.transformers.qwen2_vl import get_rope_index
33
+ from . import torch_functional as VF
34
+
35
+
36
+ def collate_fn(features: List[Dict[str, Any]]) -> Dict[str, Any]:
37
+ tensors = defaultdict(list)
38
+ non_tensors = defaultdict(list)
39
+ for feature in features:
40
+ for key, value in feature.items():
41
+ if isinstance(value, torch.Tensor):
42
+ tensors[key].append(value)
43
+ else:
44
+ non_tensors[key].append(value)
45
+
46
+ for key, value in tensors.items():
47
+ tensors[key] = torch.stack(value, dim=0)
48
+
49
+ for key, value in non_tensors.items():
50
+ non_tensors[key] = np.array(value, dtype=object)
51
+
52
+ return {**tensors, **non_tensors}
53
+
54
+
55
+
56
+ def process_image(image: Union[Dict[str, Any], ImageObject, str], min_pixels: int, max_pixels: int) -> ImageObject:
57
+ if isinstance(image, dict):
58
+ if 'path' in image:
59
+ image = Image.open(image['path']).copy()
60
+ else:
61
+ image = Image.open(BytesIO(image["bytes"]))
62
+ elif isinstance(image, bytes):
63
+ image = Image.open(BytesIO(image))
64
+ elif isinstance(image, list):
65
+ image = Image.open(image[0]).copy()
66
+ # image = image[0]
67
+ elif isinstance(image, PILImage):
68
+ image = image.copy()
69
+ else:
70
+ # image = image
71
+ raise TypeError(f'Unsupported image type: {type(image)}')
72
+
73
+ if (image.width * image.height) > max_pixels:
74
+ resize_factor = math.sqrt(max_pixels / (image.width * image.height))
75
+ width, height = int(image.width * resize_factor), int(image.height * resize_factor)
76
+ image = image.resize((width, height))
77
+
78
+ if (image.width * image.height) < min_pixels:
79
+ resize_factor = math.sqrt(min_pixels / (image.width * image.height))
80
+ width, height = int(image.width * resize_factor), int(image.height * resize_factor)
81
+ image = image.resize((width, height))
82
+
83
+ if image.mode != "RGB":
84
+ image = image.convert("RGB")
85
+
86
+ return image
87
+
88
+ def process_video(
89
+ video: str, min_pixels: Optional[int], max_pixels: Optional[int], video_fps: float, return_fps: bool = False
90
+ ) -> Union[list[ImageObject], tuple[list[ImageObject], list[float]]]:
91
+ vision_info = {"video": video, "min_pixels": min_pixels, "max_pixels": max_pixels, "fps": video_fps}
92
+ return fetch_video(vision_info, return_video_sample_fps=return_fps)
93
+
94
+ class RLHFDataset(Dataset):
95
+ """
96
+ We assume the dataset contains a column that contains prompts and other information
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ data_path: str,
102
+ tokenizer: PreTrainedTokenizer,
103
+ processor: Optional[ProcessorMixin],
104
+ prompt_key: str = "prompt",
105
+ answer_key: str = "answer",
106
+ image_key: str = "images",
107
+ max_prompt_length: int = 1024,
108
+ truncation: str = "error",
109
+ format_prompt: Optional[str] = None,
110
+ max_pixels: Optional[int] = None,
111
+ min_pixels: Optional[int] = None,
112
+ filter_overlong_prompts: bool = False,
113
+ ):
114
+ self.tokenizer = tokenizer
115
+ self.processor = processor
116
+ self.prompt_key = prompt_key
117
+ self.answer_key = answer_key
118
+ self.image_key = image_key
119
+ self.max_prompt_length = max_prompt_length
120
+ self.truncation = truncation
121
+ self.max_pixels = max_pixels
122
+ self.min_pixels = min_pixels
123
+ self.filter_overlong_prompts = filter_overlong_prompts
124
+
125
+ if "@" in data_path:
126
+ data_path, data_split = data_path.split("@")
127
+ else:
128
+ data_split = "train"
129
+
130
+ if os.path.isdir(data_path):
131
+ # when we use dataset builder, we should always refer to the train split
132
+ self.dataset = load_dataset("parquet", data_dir=data_path, split="train")
133
+ elif os.path.isfile(data_path):
134
+ self.dataset = load_dataset("parquet", data_files=data_path, split="train")
135
+ else:
136
+ # load remote dataset from huggingface hub
137
+ self.dataset = load_dataset(data_path, split=data_split)
138
+
139
+ self.format_prompt = None
140
+ if format_prompt:
141
+ with open(format_prompt, encoding="utf-8") as f:
142
+ self.format_prompt = f.read()
143
+
144
+ if self.filter_overlong_prompts:
145
+ self.dataset = self.dataset.filter(self._filter_overlong_prompts, desc="Filtering overlong prompts")
146
+
147
+ def _build_messages(self, example: Dict[str, Any]) -> List[Dict[str, Any]]:
148
+ prompt_str: str = example[self.prompt_key]
149
+ if self.format_prompt:
150
+ format_prompt = Template(self.format_prompt.strip())
151
+ prompt_str = format_prompt.render(content=prompt_str)
152
+
153
+ if self.image_key in example:
154
+ # https://huggingface.co/docs/transformers/en/tasks/image_text_to_text
155
+ content_list = []
156
+ for i, content in enumerate(prompt_str.split("<image>")):
157
+ if i != 0:
158
+ content_list.append({"type": "image"})
159
+
160
+ if content:
161
+ content_list.append({"type": "text", "text": content})
162
+
163
+ return [{"role": "user", "content": content_list}]
164
+ else:
165
+ return [{"role": "user", "content": prompt_str}]
166
+
167
+ def _filter_overlong_prompts(self, example: Dict[str, Any]) -> bool:
168
+ messages = self._build_messages(example)
169
+ processing_class = self.processor if self.processor is not None else self.tokenizer
170
+ return (
171
+ len(processing_class.apply_chat_template(messages, add_generation_prompt=True)) <= self.max_prompt_length
172
+ )
173
+
174
+ def __len__(self):
175
+ return len(self.dataset)
176
+
177
+ def __getitem__(self, index):
178
+ example: dict = self.dataset[index]
179
+ messages = self._build_messages(example)
180
+
181
+ if self.image_key in example:
182
+ prompt = self.processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
183
+ raw_image_data = example.pop(self.image_key)
184
+ if not isinstance(raw_image_data, (list, tuple)):
185
+ raw_image_data = [raw_image_data]
186
+
187
+ images = [
188
+ process_image(image, min_pixels=self.min_pixels, max_pixels=self.max_pixels)
189
+ for image in raw_image_data
190
+ ]
191
+ model_inputs = self.processor(images, [prompt], add_special_tokens=False, return_tensors="pt")
192
+ input_ids = model_inputs.pop("input_ids")[0]
193
+ attention_mask = model_inputs.pop("attention_mask")[0]
194
+ example["multi_modal_data"] = {"image": raw_image_data}
195
+ else:
196
+ prompt = self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
197
+ model_inputs = self.tokenizer([prompt], add_special_tokens=False, return_tensors="pt")
198
+ input_ids = model_inputs.pop("input_ids")[0]
199
+ attention_mask = model_inputs.pop("attention_mask")[0]
200
+
201
+ if self.processor is not None and self.processor.image_processor.__class__.__name__ == "Qwen2VLImageProcessor":
202
+ # qwen2vl mrope
203
+ position_ids = get_rope_index(
204
+ self.processor,
205
+ input_ids=input_ids,
206
+ image_grid_thw=model_inputs.get("image_grid_thw"),
207
+ attention_mask=attention_mask,
208
+ ) # (3, seq_length)
209
+ else:
210
+ position_ids = torch.clip(attention_mask.cumsum(dim=0) - 1, min=0, max=None) # (seq_length,)
211
+
212
+ input_ids, attention_mask, position_ids = VF.postprocess_data(
213
+ input_ids=input_ids,
214
+ attention_mask=attention_mask,
215
+ position_ids=position_ids,
216
+ max_length=self.max_prompt_length,
217
+ pad_token_id=self.tokenizer.pad_token_id,
218
+ left_pad=True,
219
+ truncation=self.truncation,
220
+ )
221
+ raw_prompt_ids = self.tokenizer.encode(prompt, add_special_tokens=False)
222
+ if len(raw_prompt_ids) > self.max_prompt_length:
223
+ if self.truncation == "left":
224
+ raw_prompt_ids = raw_prompt_ids[-self.max_prompt_length :]
225
+ elif self.truncation == "right":
226
+ raw_prompt_ids = raw_prompt_ids[: self.max_prompt_length]
227
+ elif self.truncation == "error":
228
+ raise RuntimeError(f"Prompt length {len(raw_prompt_ids)} is longer than {self.max_prompt_length}.")
229
+
230
+ example["input_ids"] = input_ids
231
+ example["attention_mask"] = attention_mask
232
+ example["position_ids"] = position_ids
233
+ example["raw_prompt_ids"] = raw_prompt_ids
234
+ example["ground_truth"] = example.pop(self.answer_key)
235
+ return example