Guangming Sheng commited on
Commit
d2a1fa1
·
unverified ·
1 Parent(s): 16a8b21

[ci] feat: add more CI workflow (#38)

Browse files

* [ci] upload several tests

* [ci] add sanity and tensordict utility workflow

* [ci] fix workflow

* try fix import ci

* [dataproto] update repeat and unpad/pad

* fix rollout test to 2GPU

* add a fsdp vllm hybridengine script, which can be launched by torchrun

* fix import test

* update requirement.txt

* draft vllm fsdp test

* update label

* fix

* upload conda

* test conda

* test ci

* use docker

* test ci

* test ci

* test ci

* update ci

* test ci

* fix model loader

* fix model loader

* test ci

* test

* upload e2e digit completion test

* update running script for e2e test

* update test config

* fix path

* test

* fix import to register autotokenizer

* fix tokenizer

* fix create dataset

* fix

* fix reward model validate

* fix reward module of digit_completion

* fix reward module of digit_completion

* fix reward module of digit_completion

* fix reward module of digit_completion

* fix reward module of digit_completion

* can run but seems to have some test issue

* no problem, add check results

* add e2e training

* l20-0 seems has docker permission problem, test later

* fix

* test l20-0 and torchrun

* test l20-0 and torchrun

* fix

* fix

* fix

* fix

* fix

* tolerate difference

* tolerate difference with levenshtein

* lint

* add more test for ray

* delete

* use docker on l20

* use docker on l20

* add upgrade

* update ci

* delete code

* ignore test

* upgrade ray

* fix workerhelper method

* lint

* revert worker changes

* fix

* fix

* fix

* fix worker missing func

Files changed (36) hide show
  1. .github/workflows/{gpu_test.yml → dataset.yml} +5 -5
  2. .github/workflows/e2e_gpu.yml +38 -0
  3. .github/workflows/ray_test.yml +42 -0
  4. .github/workflows/sanity.yml +39 -0
  5. .github/workflows/vllm.yml +42 -0
  6. .github/workflows/yapf_format.yml +1 -1
  7. requirements.txt +1 -0
  8. tests/e2e/__init__.py +0 -0
  9. tests/e2e/arithmetic_sequence/data/create_dataset.py +46 -0
  10. tests/e2e/arithmetic_sequence/data/test.parquet +0 -0
  11. tests/e2e/arithmetic_sequence/data/train.parquet +0 -0
  12. tests/e2e/arithmetic_sequence/model/config.json +29 -0
  13. tests/e2e/arithmetic_sequence/model/create_model_tokenizer.py +61 -0
  14. tests/e2e/arithmetic_sequence/model/generation_config.json +6 -0
  15. tests/e2e/arithmetic_sequence/model/model.safetensors +3 -0
  16. tests/e2e/arithmetic_sequence/model/tokenizer_config.json +18 -0
  17. tests/e2e/arithmetic_sequence/rl/README.md +37 -0
  18. tests/e2e/arithmetic_sequence/rl/config/ray_trainer.yaml +132 -0
  19. tests/e2e/arithmetic_sequence/rl/main_trainer.py +161 -0
  20. tests/e2e/check_results.py +52 -0
  21. tests/e2e/envs/__init__.py +17 -0
  22. tests/e2e/envs/digit_completion/__init__.py +22 -0
  23. tests/e2e/envs/digit_completion/task.py +177 -0
  24. tests/e2e/envs/digit_completion/tokenizer.py +158 -0
  25. tests/e2e/run_ray_trainer.sh +17 -0
  26. tests/gpu_utility/test_memory_buffers.py +70 -0
  27. tests/gpu_utility/test_ops.py +47 -0
  28. tests/gpu_utility/test_torch_functional.py +81 -0
  29. tests/ray/test_data_transfer.py +1 -1
  30. tests/ray/test_remote_api.py +0 -89
  31. tests/rollout/run_fsdp_vllm.py +138 -0
  32. tests/rollout/test_vllm_hf_loader.py +174 -0
  33. tests/sanity/test_import.py +23 -0
  34. verl/single_controller/base/worker.py +6 -1
  35. verl/third_party/vllm/vllm_v_0_6_3/model_loader.py +6 -0
  36. verl/trainer/ppo/ray_trainer.py +1 -1
.github/workflows/{gpu_test.yml → dataset.yml} RENAMED
@@ -1,4 +1,4 @@
1
- name: ray
2
 
3
  on:
4
  # Trigger the workflow on push or pull request,
@@ -8,13 +8,13 @@ on:
8
  - main
9
  paths:
10
  - "**/*.py"
11
- - .github/workflows/ray_test.yml
12
  pull_request:
13
  branches:
14
  - main
15
  paths:
16
  - "**/*.py"
17
- - .github/workflows/ray_test.yml
18
 
19
  jobs:
20
  ray:
@@ -30,7 +30,7 @@ jobs:
30
  run: |
31
  [ ! -d "$HOME/verl-data" ] && git clone --depth 1 https://github.com/eric-haibin-lin/verl-data ~/verl-data
32
  pytest -s -x tests/verl
33
- - name: Running ray tests that need 2 GPUs
34
  run: |
35
  cd tests/ray
36
- pytest -s -x test_rvdz.py test_driverfunc_to_worker.py test_data_transfer.py test_colocated_workers.py test_check_worker_alive.py
 
1
+ name: dataset
2
 
3
  on:
4
  # Trigger the workflow on push or pull request,
 
8
  - main
9
  paths:
10
  - "**/*.py"
11
+ - .github/workflows/dataset.yml
12
  pull_request:
13
  branches:
14
  - main
15
  paths:
16
  - "**/*.py"
17
+ - .github/workflows/dataset.yml
18
 
19
  jobs:
20
  ray:
 
30
  run: |
31
  [ ! -d "$HOME/verl-data" ] && git clone --depth 1 https://github.com/eric-haibin-lin/verl-data ~/verl-data
32
  pytest -s -x tests/verl
33
+ - name: Running ray test using cupy (move it to L20 when dockerfile ready)
34
  run: |
35
  cd tests/ray
36
+ pytest -s -x test_rvdz.py
.github/workflows/e2e_gpu.yml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: e2e_gpu
2
+
3
+ on:
4
+ # Trigger the workflow on push or pull request,
5
+ # but only for the main branch
6
+ push:
7
+ branches:
8
+ - main
9
+ paths:
10
+ - "**/*.py"
11
+ - .github/workflows/e2e_gpu.yml
12
+ pull_request:
13
+ branches:
14
+ - main
15
+ paths:
16
+ - "**/*.py"
17
+ - .github/workflows/e2e_gpu.yml
18
+
19
+ jobs:
20
+ e2e_gpu:
21
+ runs-on: [self-hosted, l20-1]
22
+ env:
23
+ HTTP_PROXY: ${{ secrets.PROXY_HTTP }}
24
+ HTTPS_PROXY: ${{ secrets.PROXY_HTTPS }}
25
+ NO_PROXY: "localhost,127.0.0.1"
26
+ container:
27
+ image: verlai/verl:vemlp-th2.4.0-cu124-vllm0.6.3-ray2.10-te1.7-v0.0.3
28
+ options: --gpus all --shm-size=10g
29
+ steps:
30
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
31
+ with:
32
+ fetch-depth: 0
33
+ - name: Install the current repository
34
+ run: |
35
+ pip3 install -e .[test]
36
+ - name: Running digit completon e2e training tests on 8 L20 GPUs
37
+ run: |
38
+ bash tests/e2e/run_ray_trainer.sh
.github/workflows/ray_test.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: ray
2
+
3
+ on:
4
+ # Trigger the workflow on push or pull request,
5
+ # but only for the main branch
6
+ push:
7
+ branches:
8
+ - main
9
+ paths:
10
+ - "**/*.py"
11
+ - .github/workflows/ray_test.yml
12
+ pull_request:
13
+ branches:
14
+ - main
15
+ paths:
16
+ - "**/*.py"
17
+ - .github/workflows/ray_test.yml
18
+
19
+ jobs:
20
+ ray:
21
+ runs-on: [self-hosted, l20-0]
22
+ env:
23
+ HTTP_PROXY: ${{ secrets.PROXY_HTTP }}
24
+ HTTPS_PROXY: ${{ secrets.PROXY_HTTPS }}
25
+ NO_PROXY: "localhost,127.0.0.1"
26
+ HF_HUB_ENABLE_HF_TRANSFER: 1
27
+ container:
28
+ image: verlai/verl:vemlp-th2.4.0-cu124-vllm0.6.3-ray2.10-te1.7-v0.0.3
29
+ options: --gpus all --shm-size=10g
30
+ steps:
31
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
32
+ with:
33
+ fetch-depth: 0
34
+ - name: Install the current repository
35
+ run: |
36
+ pip install hf_transfer
37
+ pip install -e .[test]
38
+ pip install --upgrade "ray>=2.40.0"
39
+ - name: Running ray tests that need 8 GPUs
40
+ run: |
41
+ cd tests/ray
42
+ pytest -s -x --ignore=test_check_worker_alive.py --ignore=test_rvdz.py .
.github/workflows/sanity.yml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: sanity
2
+
3
+ on:
4
+ # Trigger the workflow on push or pull request,
5
+ # but only for the main branch
6
+ push:
7
+ branches:
8
+ - main
9
+ paths:
10
+ - "**/*.py"
11
+ - .github/workflows/sanity.yml
12
+ pull_request:
13
+ branches:
14
+ - main
15
+ paths:
16
+ - "**/*.py"
17
+ - .github/workflows/sanity.yml
18
+
19
+ jobs:
20
+ sanity:
21
+ runs-on: ubuntu-latest
22
+ strategy:
23
+ matrix:
24
+ python-version: ["3.10"]
25
+ steps:
26
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27
+ - name: Set up Python ${{ matrix.python-version }}
28
+ uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ - name: Install the current repository
32
+ run: |
33
+ pip install -e .[test]
34
+ - name: Run sanity test
35
+ run: |
36
+ pytest -s -x tests/sanity
37
+ - name: Run untility test
38
+ run: |
39
+ pytest -s -x tests/utility
.github/workflows/vllm.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: vllm
2
+
3
+ on:
4
+ # Trigger the workflow on push or pull request,
5
+ # but only for the main branch
6
+ push:
7
+ branches:
8
+ - main
9
+ paths:
10
+ - "**/*.py"
11
+ - .github/workflows/vllm.yml
12
+ pull_request:
13
+ branches:
14
+ - main
15
+ paths:
16
+ - "**/*.py"
17
+ - .github/workflows/vllm.yml
18
+
19
+ jobs:
20
+ vllm:
21
+ runs-on: [self-hosted, l20-0]
22
+ env:
23
+ HTTP_PROXY: ${{ secrets.PROXY_HTTP }}
24
+ HTTPS_PROXY: ${{ secrets.PROXY_HTTPS }}
25
+ NO_PROXY: "localhost,127.0.0.1"
26
+ HF_HUB_ENABLE_HF_TRANSFER: 1
27
+ container:
28
+ image: verlai/verl:vemlp-th2.4.0-cu124-vllm0.6.3-ray2.10-te1.7-v0.0.3
29
+ options: --gpus all --shm-size=10g
30
+ steps:
31
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
32
+ with:
33
+ fetch-depth: 0
34
+ - name: Install the current repository
35
+ run: |
36
+ pip3 install hf_transfer
37
+ pip3 install -e .[test]
38
+ pip3 install vllm==0.5.4
39
+ - name: Running vllm tests on 8 L20 GPUs
40
+ run: |
41
+ cd tests/rollout
42
+ torchrun --standalone --nnodes=1 --nproc_per_node=8 $(which pytest) -s test_vllm_hf_loader.py
.github/workflows/yapf_format.yml CHANGED
@@ -38,7 +38,7 @@ jobs:
38
  - name: Install dependencies
39
  run: |
40
  python -m pip install --upgrade pip
41
- pip install yapf
42
  pip install toml==0.10.2
43
  - name: Running yapf
44
  run: |
 
38
  - name: Install dependencies
39
  run: |
40
  python -m pip install --upgrade pip
41
+ pip install --upgrade yapf
42
  pip install toml==0.10.2
43
  - name: Running yapf
44
  run: |
requirements.txt CHANGED
@@ -4,6 +4,7 @@ datasets
4
  dill
5
  hydra-core
6
  numpy
 
7
  pybind11
8
  ray
9
  tensordict<0.6
 
4
  dill
5
  hydra-core
6
  numpy
7
+ pandas
8
  pybind11
9
  ray
10
  tensordict<0.6
tests/e2e/__init__.py ADDED
File without changes
tests/e2e/arithmetic_sequence/data/create_dataset.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
+ from tests.e2e.envs.digit_completion import DigitCompletion, generate_ground_truth_response
16
+ from torch.utils import data
17
+ import os
18
+
19
+ if __name__ == '__main__':
20
+ simple_task = DigitCompletion(max_number=9, max_diff=9, max_num_in_response=9)
21
+ all_prompts = simple_task.get_all_prompts()
22
+
23
+ # 21 * 6 * 4
24
+ train_data, test_data = data.random_split(all_prompts, lengths=[0.8, 0.2])
25
+ train_data = list(train_data)
26
+ test_data = list(test_data)
27
+
28
+ train_data = [[{'role': 'user', 'content': str(item)}] \
29
+ for item in train_data]
30
+ test_data = [[{'role': 'user', 'content': str(item)}] \
31
+ for item in test_data]
32
+
33
+ print(f'Size of train: {len(train_data)}, size of test: {len(test_data)}')
34
+
35
+ train_data = {'prompt': train_data}
36
+ test_data = {'prompt': test_data}
37
+
38
+ model_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)))
39
+
40
+ import pandas as pd
41
+
42
+ train_data_frame = pd.DataFrame(train_data)
43
+ test_data_frame = pd.DataFrame(test_data)
44
+
45
+ train_data_frame.to_parquet(os.path.join(model_folder, 'train.parquet'))
46
+ test_data_frame.to_parquet(os.path.join(model_folder, 'test.parquet'))
tests/e2e/arithmetic_sequence/data/test.parquet ADDED
Binary file (3.15 kB). View file
 
tests/e2e/arithmetic_sequence/data/train.parquet ADDED
Binary file (6.23 kB). View file
 
tests/e2e/arithmetic_sequence/model/config.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": null,
8
+ "eos_token_id": 1,
9
+ "hidden_act": "silu",
10
+ "hidden_size": 128,
11
+ "initializer_range": 0.02,
12
+ "intermediate_size": 344,
13
+ "max_position_embeddings": 2048,
14
+ "mlp_bias": false,
15
+ "model_type": "llama",
16
+ "num_attention_heads": 4,
17
+ "num_hidden_layers": 4,
18
+ "num_key_value_heads": 4,
19
+ "pad_token_id": 2,
20
+ "pretraining_tp": 1,
21
+ "rms_norm_eps": 1e-06,
22
+ "rope_scaling": null,
23
+ "rope_theta": 10000.0,
24
+ "tie_word_embeddings": false,
25
+ "torch_dtype": "bfloat16",
26
+ "transformers_version": "4.43.3",
27
+ "use_cache": true,
28
+ "vocab_size": 16
29
+ }
tests/e2e/arithmetic_sequence/model/create_model_tokenizer.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Create a random model and tokenizer for PPO training
16
+ """
17
+
18
+ import torch
19
+ import os
20
+ from transformers import AutoModelForCausalLM, LlamaConfig, AutoTokenizer
21
+
22
+ from tests.e2e.envs.digit_completion import CharTokenizer
23
+
24
+ tokenizer = CharTokenizer(
25
+ characters=['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', ':'],
26
+ model_max_length=2048,
27
+ chat_template=
28
+ "{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set role = message['role'] %}{{ message['content'] }}{% endfor %}{% if add_generation_prompt %}{{ sep_token }}{% endif %}"
29
+ )
30
+
31
+ config = LlamaConfig(vocab_size=(tokenizer.vocab_size + 16 - 1) // 16 * 16,
32
+ hidden_size=128,
33
+ intermediate_size=344,
34
+ num_hidden_layers=4,
35
+ num_attention_heads=4,
36
+ num_key_value_heads=4,
37
+ pad_token_id=tokenizer.pad_token_id,
38
+ bos_token_id=tokenizer.bos_token_id,
39
+ eos_token_id=tokenizer.eos_token_id)
40
+
41
+ model = AutoModelForCausalLM.from_config(config, torch_dtype=torch.bfloat16)
42
+
43
+ model_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)))
44
+ os.makedirs(model_folder, exist_ok=True)
45
+
46
+ model.save_pretrained(model_folder)
47
+
48
+ tokenizer_folder = model_folder
49
+ tokenizer.save_pretrained(tokenizer_folder)
50
+
51
+ load_tokenizer = AutoTokenizer.from_pretrained(tokenizer_folder)
52
+
53
+ chat = [{'role': 'user', 'content': '1,0:2,3'}]
54
+
55
+ load_tokenizer.padding_side = 'left'
56
+ print(
57
+ load_tokenizer.apply_chat_template(chat,
58
+ tokenize=True,
59
+ add_generation_prompt=True,
60
+ max_length=10,
61
+ padding='max_length'))
tests/e2e/arithmetic_sequence/model/generation_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": 1,
4
+ "pad_token_id": 2,
5
+ "transformers_version": "4.43.3"
6
+ }
tests/e2e/arithmetic_sequence/model/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3dcf33555c1207f9989d5fba5dc4d8b935c55736456e9b76b604a45215ac5cc4
3
+ size 1595672
tests/e2e/arithmetic_sequence/model/tokenizer_config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "char_ords": [
3
+ 48,
4
+ 49,
5
+ 50,
6
+ 51,
7
+ 52,
8
+ 53,
9
+ 54,
10
+ 55,
11
+ 56,
12
+ 57,
13
+ 44,
14
+ 58
15
+ ],
16
+ "model_max_length": 2048,
17
+ "chat_template": "{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set role = message['role'] %}{{ message['content'] }}{% endfor %}{% if add_generation_prompt %}{{ sep_token }}{% endif %}"
18
+ }
tests/e2e/arithmetic_sequence/rl/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Digit completion
2
+
3
+ This is an example of solving a digit completion problem. The problem is defined as below:
4
+
5
+ The prompt is a sequence of numbers with fixed difference. The agent's goal is to complete the next N numbers.
6
+ If the max number is reached, the next number should be modulo with max number.
7
+
8
+ For example,
9
+ - prompt = [1, 2, 3]
10
+ - N = 5
11
+ - max_number = 6
12
+
13
+ The response should be [4, 5, 6, 7%6, 8%6] = [4, 5, 6, 0, 1].
14
+
15
+ # Environment definition
16
+
17
+ The core definition of the task is defined in verl/envs/digit_completion/task.py
18
+
19
+ It is highly recommended to take a look at it for better understanding.
20
+
21
+
22
+
23
+ # Run experiments
24
+
25
+ The users are required to specify the config path and config name (and the relative model config path to the current working directory)
26
+
27
+ ```bash
28
+ # cd examples/arithmetic_sequence/rl
29
+
30
+ # Specify the config path and config name (current working dir)
31
+ python3 -m verl.trainer.ppo.ray_megatron_train_synchronous --config-path=$(pwd)/config --config-name='ray_megatron'
32
+
33
+ # The default relative path of model config is 'config/model_config', if you want to change it, you can rewrite it in ray_megatron.yaml or using:
34
+ python3 -m verl.trainer.ppo.ray_megatron_train_synchronous --config-path=$(pwd)/config --config-name='ray_megatron' ++model.base_path=config/model_config
35
+
36
+ ```
37
+
tests/e2e/arithmetic_sequence/rl/config/ray_trainer.yaml ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data:
2
+ tokenizer: null
3
+ train_files: ~/verl/tests/e2e/arithmetic_sequence/data/train.parquet
4
+ val_files: ~/verl/tests/e2e/arithmetic_sequence/data/test.parquet
5
+ prompt_key: prompt
6
+ max_prompt_length: 16
7
+ max_response_length: 32
8
+ train_batch_size: 800
9
+ val_batch_size: 200
10
+ return_raw_input_ids: True # This should be set to true when the tokenizer between policy and rm differs
11
+ return_raw_chat: False
12
+
13
+ actor_rollout_ref:
14
+ hybrid_engine: True
15
+ model:
16
+ path: ~/verl/tests/e2e/arithmetic_sequence/model
17
+ external_lib: tests.e2e.envs.digit_completion
18
+ override_config: {}
19
+ enable_gradient_checkpointing: False
20
+ actor:
21
+ strategy: fsdp # This is for backward-compatibility
22
+ ppo_mini_batch_size: 200
23
+ ppo_micro_batch_size: 200
24
+ grad_clip: 1.0
25
+ clip_ratio: 0.2
26
+ entropy_coeff: 0.0
27
+ ppo_epochs: 1
28
+ shuffle: True
29
+ optim:
30
+ lr: 1e-4
31
+ fsdp_config:
32
+ wrap_policy:
33
+ # transformer_layer_cls_to_wrap: None
34
+ min_num_params: 0
35
+ param_offload: False
36
+ grad_offload: False
37
+ optimizer_offload: False
38
+ ref:
39
+ fsdp_config:
40
+ param_offload: False
41
+ wrap_policy:
42
+ # transformer_layer_cls_to_wrap: None
43
+ min_num_params: 0
44
+ micro_batch_size: 200
45
+ rollout:
46
+ name: hf
47
+ temperature: 1.0
48
+ top_k: -1 # 0 for hf rollout, -1 for vllm rollout
49
+ top_p: 1
50
+ prompt_length: ${data.max_prompt_length} # for xperf_gpt
51
+ response_length: ${data.max_response_length}
52
+ # for vllm rollout
53
+ dtype: bfloat16 # should align with FSDP
54
+ gpu_memory_utilization: 0.1
55
+ ignore_eos: False
56
+ micro_batch_size: 200
57
+ enforce_eager: True
58
+ free_cache_engine: True
59
+ load_format: dummy_dtensor
60
+ tensor_model_parallel_size: 2
61
+ max_num_batched_tokens: 8192
62
+ max_num_seqs: 1024
63
+ log_prob_micro_batch_size: 200
64
+ # for hf rollout
65
+ do_sample: True
66
+ # number of responses (i.e. num sample times)
67
+ n: 1 # > 1 for grpo
68
+
69
+ critic:
70
+ strategy: fsdp
71
+ optim:
72
+ lr: 1e-3
73
+ model:
74
+ path: ~/verl/tests/e2e/arithmetic_sequence/model
75
+ tokenizer_path: ${actor_rollout_ref.model.path}
76
+ override_config: {}
77
+ external_lib: ${actor_rollout_ref.model.external_lib}
78
+ enable_gradient_checkpointing: False
79
+ fsdp_config:
80
+ param_offload: False
81
+ grad_offload: False
82
+ optimizer_offload: False
83
+ wrap_policy:
84
+ # transformer_layer_cls_to_wrap: None
85
+ min_num_params: 0
86
+ ppo_mini_batch_size: ${actor_rollout_ref.actor.ppo_mini_batch_size}
87
+ ppo_micro_batch_size: 200
88
+ forward_micro_batch_size: ${critic.ppo_micro_batch_size}
89
+ ppo_epochs: ${actor_rollout_ref.actor.ppo_epochs}
90
+ shuffle: ${actor_rollout_ref.actor.shuffle}
91
+ grad_clip: 1.0
92
+ cliprange_value: 0.5
93
+
94
+ # the following parameters are for backward-compatibility and should be removed
95
+ kl_ctrl:
96
+ type: fixed
97
+ kl_coef: 0.001
98
+
99
+ reward_model:
100
+ strategy: fsdp
101
+ enable: False
102
+ model:
103
+ input_tokenizer: ${actor_rollout_ref.model.path} # set this to null if the chat template is identical
104
+ path: ~/models/FsfairX-LLaMA3-RM-v0.1
105
+ external_lib: ${actor_rollout_ref.model.external_lib}
106
+ offload: False
107
+ fsdp_config:
108
+ min_num_params: 0
109
+ micro_batch_size: 8
110
+ max_length: null
111
+
112
+ algorithm:
113
+ gamma: 1.0
114
+ lam: 1.0
115
+ adv_estimator: gae
116
+ kl_penalty: kl # how to estimate kl divergence
117
+ kl_ctrl:
118
+ type: fixed
119
+ kl_coef: 0.005
120
+
121
+ trainer:
122
+ total_epochs: 200
123
+ project_name: verl_examples
124
+ experiment_name: arithmetic_sequences
125
+ logger: ['console']
126
+ nnodes: 1
127
+ n_gpus_per_node: 1
128
+ save_freq: -1
129
+ test_freq: 1
130
+ critic_warmup: 0
131
+ default_hdfs_dir: ~/experiments/gsm8k/ppo/${trainer.experiment_name}
132
+ default_local_dir: checkpoints/${trainer.project_name}/${trainer.experiment_name}
tests/e2e/arithmetic_sequence/rl/main_trainer.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Using FSDPTrainer
16
+ """
17
+ import re
18
+ import os
19
+ import hydra
20
+ import numpy as np
21
+ import ray
22
+ import torch
23
+ from torch.utils.data import DataLoader
24
+ from transformers import PreTrainedTokenizer, AutoTokenizer
25
+
26
+ import verl.utils.torch_functional as verl_F
27
+ from verl import DataProto
28
+ from verl.trainer.ppo.ray_trainer import RayPPOTrainer
29
+ from verl.utils.fs import copy_local_path_from_hdfs
30
+ from verl.utils.model import compute_position_id_with_mask
31
+ from tests.e2e.envs.digit_completion import CharTokenizer
32
+ import pandas as pd
33
+
34
+
35
+ def make_reward_function(tokenizer, num_examine):
36
+
37
+ def arithmetic_sequence_reward_function(data: DataProto):
38
+ from tests.e2e.envs.digit_completion.task import compute_reward
39
+ reward_tensor = torch.zeros_like(data.batch['responses'], dtype=torch.float32)
40
+
41
+ for i in range(data.batch.batch_size[0]):
42
+ data_item = data[i] # DataProtoItem
43
+
44
+ prompt_ids = data_item.batch['prompts']
45
+
46
+ prompt_length = prompt_ids.shape[-1]
47
+
48
+ # extract raw prompt
49
+ valid_prompt_length = data_item.batch['attention_mask'][:prompt_length].sum()
50
+ valid_prompt_ids = prompt_ids[-valid_prompt_length:]
51
+
52
+ # extract response
53
+ response_ids = data_item.batch['responses']
54
+ response_length = response_ids.shape[-1]
55
+ response_mask = data.batch['attention_mask'][i][-response_length:]
56
+ valid_response_length = data_item.batch['attention_mask'][prompt_length:].sum()
57
+ valid_response_ids = response_ids[:valid_response_length]
58
+
59
+ # decode
60
+ prompt = tokenizer.decode(valid_prompt_ids)
61
+ response = tokenizer.decode(valid_response_ids)
62
+ # remove bos and eos
63
+ prompt = prompt.replace(tokenizer.sep_token, '')
64
+ response = response.replace(tokenizer.eos_token, '')
65
+ if i < num_examine:
66
+ print(prompt, response)
67
+
68
+ reward_output = compute_reward(prompt, response)
69
+ dense_reward = reward_output[0].tolist()
70
+ ground_truth_response = reward_output[1]['ground_truth_response']
71
+ if len(dense_reward) > 0:
72
+ last_reward = dense_reward[-1]
73
+ else:
74
+ if len(ground_truth_response) == 0:
75
+ last_reward = 1
76
+ else:
77
+ last_reward = 0
78
+
79
+ # pad to response_length
80
+ for _ in range(reward_tensor.shape[-1] - len(dense_reward)):
81
+ dense_reward.append(last_reward)
82
+
83
+ dense_reward = torch.as_tensor(dense_reward, dtype=torch.float32, device=reward_tensor.device)
84
+ reward_tensor[i] = dense_reward * response_mask
85
+
86
+ return reward_tensor
87
+
88
+ return arithmetic_sequence_reward_function
89
+
90
+
91
+ @hydra.main(config_path='config', config_name='ray_trainer', version_base=None)
92
+ def main(config):
93
+ ray.init(
94
+ runtime_env={
95
+ 'env_vars': {
96
+ 'MEGATRON_USE_CUDA_TIMER': '0',
97
+ 'MEGATRON_START_PROCESS_TIMER': 'False',
98
+ 'TOKENIZERS_PARALLELISM': 'true',
99
+ 'NCCL_DEBUG': 'WARN'
100
+ }
101
+ })
102
+
103
+ # print initial config
104
+ from pprint import pprint
105
+ from omegaconf import OmegaConf
106
+ pprint(OmegaConf.to_container(config, resolve=True)) # resolve=True will eval symbol values
107
+
108
+ dp_size = config.trainer.n_gpus_per_node * config.trainer.nnodes
109
+ # normalize batch_size
110
+ # TODO: move this inside each role
111
+ config.actor_rollout_ref.actor.ppo_mini_batch_size //= dp_size
112
+ config.actor_rollout_ref.actor.ppo_micro_batch_size //= dp_size
113
+ config.critic.ppo_micro_batch_size //= dp_size
114
+ config.actor_rollout_ref.rollout.micro_batch_size //= dp_size
115
+
116
+ # print the config
117
+ # print initial config
118
+ print('Config after normalizing batch_size')
119
+ pprint(OmegaConf.to_container(config, resolve=True)) # resolve=True will eval symbol values
120
+
121
+ # download the checkpoint from hdfs
122
+ local_path = copy_local_path_from_hdfs(config.actor_rollout_ref.model.path)
123
+ local_path = os.path.expanduser(local_path)
124
+ # instantiate tokenizern
125
+ tokenizer = AutoTokenizer.from_pretrained(local_path)
126
+ print(f'Tokenizer vocab_size: {tokenizer.vocab_size}')
127
+
128
+ # define worker classes
129
+ from verl.workers.fsdp_workers import ActorRolloutRefWorker, CriticWorker
130
+ from verl.trainer.ppo.ray_trainer import ResourcePoolManager, Role
131
+
132
+ role_worker_mapping = {
133
+ Role.ActorRollout: ray.remote(ActorRolloutRefWorker),
134
+ Role.Critic: ray.remote(CriticWorker),
135
+ }
136
+
137
+ global_pool_id = 'global_pool'
138
+ resource_pool_spec = {
139
+ global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes,
140
+ }
141
+ mapping = {
142
+ Role.ActorRollout: global_pool_id,
143
+ Role.Critic: global_pool_id,
144
+ }
145
+
146
+ reward_fn = make_reward_function(tokenizer=tokenizer, num_examine=1)
147
+
148
+ resource_pool_manager = ResourcePoolManager(resource_pool_spec=resource_pool_spec, mapping=mapping)
149
+
150
+ trainer = RayPPOTrainer(config=config,
151
+ tokenizer=tokenizer,
152
+ role_worker_mapping=role_worker_mapping,
153
+ resource_pool_manager=resource_pool_manager,
154
+ reward_fn=reward_fn,
155
+ val_reward_fn=reward_fn)
156
+ trainer.init_workers()
157
+ trainer.fit()
158
+
159
+
160
+ if __name__ == '__main__':
161
+ main()
tests/e2e/check_results.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 argparse
16
+
17
+ import numpy as np
18
+
19
+
20
+ def extract_reward_from_line(line):
21
+ # TODO: this function needs error handling
22
+ try:
23
+ key_vals = line.split(' - ')
24
+ for key_val in key_vals:
25
+ key, val = key_val.split(':')
26
+ if key == 'critic/rewards/mean':
27
+ reward = float(val)
28
+ return reward
29
+ return -np.inf
30
+ except Exception:
31
+ return -np.inf
32
+
33
+
34
+ if __name__ == '__main__':
35
+ parser = argparse.ArgumentParser()
36
+ parser.add_argument('--output_file', required=True, type=str)
37
+
38
+ args = parser.parse_args()
39
+
40
+ with open(args.output_file, 'r') as f:
41
+ output = f.read().split('\n')
42
+
43
+ best_reward = -np.inf
44
+ for line in output:
45
+ if line.startswith('step'):
46
+ reward = extract_reward_from_line(line)
47
+ if reward > best_reward:
48
+ best_reward = reward
49
+
50
+ print(f'Best reward is {best_reward}')
51
+ assert best_reward > 0.2, f'Best reward must be greater than 0.3. best_reward: {best_reward}'
52
+ print('Check passes')
tests/e2e/envs/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 .digit_completion import DigitCompletion
16
+
17
+ __all__ = ['DigitCompletion']
tests/e2e/envs/digit_completion/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 .task import DigitCompletion, generate_ground_truth_response
16
+ from .tokenizer import CharTokenizer
17
+
18
+ from transformers import AutoTokenizer, LlamaConfig
19
+
20
+ AutoTokenizer.register(LlamaConfig, CharTokenizer, exist_ok=True)
21
+
22
+ __all__ = ['DigitCompletion', 'generate_ground_truth_response', 'CharTokenizer']
tests/e2e/envs/digit_completion/task.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Task and environment definition for digit completion."""
15
+
16
+ import numpy as np
17
+
18
+
19
+ class DigitCompletion(object):
20
+ """
21
+ The implementation of a simple digit completion task.
22
+ The prompt is a sequence of numbers with fixed difference. The task is to complete the next N numbers.
23
+ If the max number is reached, the next number should be modulo with max number.
24
+
25
+ For example,
26
+ - prompt = [1, 2, 3]
27
+ - N = 5
28
+ - max_number = 6
29
+
30
+ the response should be [4, 5, 6, 7%6, 8%6] = [4, 5, 6, 0, 1]
31
+
32
+ Note that the tokenizer is char-level to increase the difficulty.
33
+ """
34
+
35
+ def __init__(self, max_number: int, max_diff: int, max_num_in_response: int, seed=0):
36
+ """
37
+
38
+ Args:
39
+ max_number: the maximum number allowed in the arithmetic sequence
40
+ max_diff: the maximum diff. The actual common diff will be sampled from [0, max_diff]
41
+ max_num_in_response: the maximum number in the response
42
+ """
43
+ super().__init__()
44
+ self.max_number = max_number
45
+ self.max_diff = max_diff
46
+ self.max_num_in_response = max_num_in_response
47
+ assert self.max_num_in_response < 10
48
+ assert self.max_number > 0
49
+ assert self.max_diff > 0
50
+ self.max_number_length = len(str(max_number))
51
+ # {num1},{num2}:{max_num_in_response},{max_number}
52
+ self._prompt_length = self.max_number_length * 2 + 4 + self.max_number_length # no negative is allowed
53
+
54
+ self.np_rng = np.random.default_rng(seed=seed)
55
+
56
+ def __str__(self):
57
+ return f'Prompt length: {self.prompt_length}. Response length: {self.response_length}, ' \
58
+ f'Max number: {self.max_number}. Max diff: {self.max_diff}, ' \
59
+ f'Max number in response: {self.max_num_in_response}'
60
+
61
+ def get_state(self):
62
+ return {'rng': self.np_rng}
63
+
64
+ def set_state(self, state):
65
+ assert 'rng' in state, 'rng must be inside state'
66
+ self.np_rng = state['rng']
67
+
68
+ @property
69
+ def prompt_length(self):
70
+ return self._prompt_length
71
+
72
+ @property
73
+ def response_length(self):
74
+ # number length + comma length + [EOS]
75
+ # The actual number times 1.5 to allow 'U'
76
+ return (self.max_num_in_response * self.max_number_length + (self.max_num_in_response - 1) + 1) * 2
77
+
78
+ def add(self, a, b):
79
+ return (a + b) % self.max_number
80
+
81
+ def get_all_prompts(self):
82
+ all_prompts = []
83
+ for first_num in range(self.max_number + 1):
84
+ for diff in range(0, self.max_diff + 1):
85
+ second_num = self.add(first_num, diff)
86
+ for num_to_complete in range(self.max_num_in_response + 1):
87
+ prompt = str(first_num) + ',' + str(second_num) + f':{self.max_number},{num_to_complete}'
88
+ all_prompts.append(prompt)
89
+ return all_prompts
90
+
91
+ def sample_str_prompts(self):
92
+ # step 1: sample initial numbers
93
+ first_num = self.np_rng.integers(self.max_number + 1)
94
+ diff = self.np_rng.integers(self.max_diff + 1)
95
+ second_num = self.add(first_num, diff)
96
+ num_to_complete = self.np_rng.integers(self.max_num_in_response + 1)
97
+ prompt = str(first_num) + ',' + str(second_num) + f':{self.max_number},{num_to_complete}'
98
+ return prompt
99
+
100
+ def sample_batch_str_prompts(self, batch_size):
101
+ str_prompts = []
102
+ for _ in range(batch_size):
103
+ str_prompts.append(self.sample_str_prompts())
104
+ return str_prompts
105
+
106
+
107
+ def compute_attention_mask(prompts, pad_token_id):
108
+ mask = np.ones_like(prompts)
109
+ mask[prompts == pad_token_id] = 0
110
+ return mask
111
+
112
+
113
+ def compute_position_id_with_mask(mask):
114
+ return np.clip(np.cumsum(mask, axis=-1) - 1, a_min=0, a_max=None)
115
+
116
+
117
+ def generate_ground_truth_response(prompt: str):
118
+ """Generate ground truth response given a prompt."""
119
+ num, info = prompt.split(':')
120
+ num1, num2 = num.split(',')
121
+ max_number, num_to_gen = info.split(',')
122
+ num1 = int(num1)
123
+ num2 = int(num2)
124
+ max_number = int(max_number)
125
+ num_to_gen = int(num_to_gen)
126
+ diff = (num2 - num1) % max_number
127
+ results = []
128
+ last_num = num2
129
+ for _ in range(num_to_gen):
130
+ curr = (last_num + diff) % max_number
131
+ results.append(str(curr))
132
+ last_num = curr
133
+ response = ','.join(results)
134
+ return response
135
+
136
+
137
+ def compute_reward(prompt: str, response: str, sequence_reward=1.):
138
+ """We compute dense reward here so that we can directly train RL without SFT"""
139
+ response_length = len(response)
140
+ ground_truth_response = generate_ground_truth_response(prompt)
141
+ per_token_reward = sequence_reward / (len(ground_truth_response) + 1) # including [EOS]
142
+
143
+ # pad
144
+ reward = np.zeros(response_length, dtype=np.float32) # this assumes that each char is a token
145
+ # assign reward until mismatches
146
+ ground_truth_idx = 0
147
+ for i in range(response_length):
148
+ if ground_truth_idx == len(ground_truth_response):
149
+ break
150
+
151
+ ground_truth_response_token = ground_truth_response[ground_truth_idx]
152
+ response_token = response[i]
153
+ if ground_truth_response_token == response_token:
154
+ reward[i] = per_token_reward
155
+ ground_truth_idx += 1
156
+ else:
157
+ # no matches
158
+ break
159
+
160
+ return reward, {'ground_truth_response': ground_truth_response}
161
+
162
+
163
+ if __name__ == '__main__':
164
+ task = DigitCompletion(max_number=20, max_diff=3, max_num_in_response=5)
165
+ print(task.sample_str_prompts())
166
+
167
+ prompt = '7,8:20,0'
168
+ response = ''
169
+ print(compute_reward(prompt, response))
170
+
171
+ prompt = '7,8:20,0'
172
+ response = 'E000'
173
+ print(compute_reward(prompt, response))
174
+
175
+ prompt = '9,10:20,2'
176
+ response = '11,12,13'
177
+ print(compute_reward(prompt, response))
tests/e2e/envs/digit_completion/tokenizer.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Copied from https://github.com/dariush-bahrami/character-tokenizer/blob/master/charactertokenizer/core.py
15
+
16
+ CharacterTokenzier for Hugging Face Transformers.
17
+
18
+ This is heavily inspired from CanineTokenizer in transformers package.
19
+ """
20
+
21
+ import json
22
+ import os
23
+ from pathlib import Path
24
+ from typing import Dict, List, Optional, Sequence, Union
25
+
26
+ from transformers.tokenization_utils import AddedToken, PreTrainedTokenizer
27
+
28
+
29
+ class CharTokenizer(PreTrainedTokenizer):
30
+
31
+ def __init__(self, characters: Sequence[str], model_max_length: int, chat_template, **kwargs):
32
+ """Character tokenizer for Hugging Face transformers.
33
+
34
+ Args:
35
+ characters (Sequence[str]): List of desired characters. Any character which
36
+ is not included in this list will be replaced by a special token called
37
+ [UNK] with id=6. Following are list of all of the special tokens with
38
+ their corresponding ids:
39
+ "[CLS]": 0
40
+ "[SEP]": 1
41
+ "[BOS]": 2
42
+ "[MASK]": 3
43
+ "[PAD]": 4
44
+ "[RESERVED]": 5
45
+ "[UNK]": 6
46
+ an id (starting at 7) will be assigned to each character.
47
+
48
+ model_max_length (int): Model maximum sequence length.
49
+ """
50
+ eos_token_str = 'E'
51
+ sep_token_str = 'S'
52
+ pad_token_str = 'P'
53
+ unk_token_str = 'U'
54
+
55
+ self.characters = characters
56
+ self.model_max_length = model_max_length
57
+ eos_token = AddedToken(eos_token_str, lstrip=False, rstrip=False)
58
+ sep_token = AddedToken(sep_token_str, lstrip=False, rstrip=False)
59
+ pad_token = AddedToken(pad_token_str, lstrip=False, rstrip=False)
60
+ unk_token = AddedToken(unk_token_str, lstrip=False, rstrip=False)
61
+
62
+ self._vocab_str_to_int = {
63
+ sep_token_str: 0,
64
+ eos_token_str: 1,
65
+ pad_token_str: 2,
66
+ unk_token_str: 3,
67
+ **{
68
+ ch: i + 4 for i, ch in enumerate(characters)
69
+ },
70
+ }
71
+ self._vocab_int_to_str = {v: k for k, v in self._vocab_str_to_int.items()}
72
+
73
+ super().__init__(
74
+ eos_token=eos_token,
75
+ sep_token=sep_token,
76
+ pad_token=pad_token,
77
+ unk_token=unk_token,
78
+ add_prefix_space=False,
79
+ model_max_length=model_max_length,
80
+ **kwargs,
81
+ )
82
+
83
+ self.chat_template = chat_template
84
+
85
+ @property
86
+ def vocab_size(self) -> int:
87
+ return len(self._vocab_str_to_int)
88
+
89
+ def get_vocab(self):
90
+ return self._vocab_str_to_int
91
+
92
+ def _tokenize(self, text: str) -> List[str]:
93
+ return list(text)
94
+
95
+ def _convert_token_to_id(self, token: str) -> int:
96
+ return self._vocab_str_to_int.get(token, self._vocab_str_to_int["U"])
97
+
98
+ def _convert_id_to_token(self, index: int) -> str:
99
+ return self._vocab_int_to_str[index]
100
+
101
+ def convert_tokens_to_string(self, tokens):
102
+ return "".join(tokens)
103
+
104
+ def build_inputs_with_special_tokens(self,
105
+ token_ids_0: List[int],
106
+ token_ids_1: Optional[List[int]] = None) -> List[int]:
107
+ sep = [self.sep_token_id]
108
+ cls = [self.cls_token_id]
109
+ result = cls + token_ids_0 + sep
110
+ if token_ids_1 is not None:
111
+ result += token_ids_1 + sep
112
+ return result
113
+
114
+ def get_special_tokens_mask(
115
+ self,
116
+ token_ids_0: List[int],
117
+ token_ids_1: Optional[List[int]] = None,
118
+ already_has_special_tokens: bool = False,
119
+ ) -> List[int]:
120
+ if already_has_special_tokens:
121
+ return super().get_special_tokens_mask(
122
+ token_ids_0=token_ids_0,
123
+ token_ids_1=token_ids_1,
124
+ already_has_special_tokens=True,
125
+ )
126
+
127
+ result = [1] + ([0] * len(token_ids_0)) + [1]
128
+ if token_ids_1 is not None:
129
+ result += ([0] * len(token_ids_1)) + [1]
130
+ return result
131
+
132
+ def get_config(self) -> Dict:
133
+ return {
134
+ "char_ords": [ord(ch) for ch in self.characters],
135
+ "model_max_length": self.model_max_length,
136
+ "chat_template": self.chat_template
137
+ }
138
+
139
+ @classmethod
140
+ def from_config(cls, config: Dict) -> "DigitCompletionTokenizer":
141
+ cfg = {}
142
+ cfg["characters"] = [chr(i) for i in config["char_ords"]]
143
+ cfg["model_max_length"] = config["model_max_length"]
144
+ cfg["chat_template"] = config["chat_template"]
145
+ return cls(**cfg)
146
+
147
+ def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
148
+ cfg_file = Path(save_directory) / "tokenizer_config.json"
149
+ cfg = self.get_config()
150
+ with open(cfg_file, "w") as f:
151
+ json.dump(cfg, f, indent=4)
152
+
153
+ @classmethod
154
+ def from_pretrained(cls, save_directory: Union[str, os.PathLike], **kwargs):
155
+ cfg_file = Path(save_directory) / "tokenizer_config.json"
156
+ with open(cfg_file) as f:
157
+ cfg = json.load(f)
158
+ return cls.from_config(cfg)
tests/e2e/run_ray_trainer.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -e -x
4
+
5
+ OUTPUT_FILE="/tmp/output_ray_trainer.txt"
6
+
7
+ export PATH=$PATH:~/.local/bin
8
+
9
+ rm -rf $OUTPUT_FILE
10
+ python3 tests/e2e/arithmetic_sequence/rl/main_trainer.py \
11
+ data.train_files=tests/e2e/arithmetic_sequence/data/train.parquet \
12
+ data.val_files=tests/e2e/arithmetic_sequence/data/test.parquet \
13
+ actor_rollout_ref.model.path=tests/e2e/arithmetic_sequence/model \
14
+ critic.model.path=tests/e2e/arithmetic_sequence/model | tee $OUTPUT_FILE;
15
+
16
+ python3 tests/e2e/check_results.py --output_file=$OUTPUT_FILE
17
+ rm -rf $OUTPUT_FILE
tests/gpu_utility/test_memory_buffers.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Test memory buffers
16
+ - We start with two models with the same weights
17
+ - We use Memory buffer to make one of the models and then compare the parameters
18
+ """
19
+
20
+ import torch
21
+ import gc
22
+
23
+ from transformers import LlamaModel, LlamaConfig
24
+ from verl.utils.memory_buffer import MemoryBufferModuleWrapper
25
+
26
+
27
+ def test_memory_buffers():
28
+ llama_config = LlamaConfig(vocab_size=256,
29
+ hidden_size=4096,
30
+ intermediate_size=11008,
31
+ num_hidden_layers=2,
32
+ num_attention_heads=16,
33
+ num_key_value_heads=16)
34
+
35
+ model = LlamaModel(config=llama_config).cuda()
36
+ model_copy = LlamaModel(config=llama_config).cuda()
37
+ model_copy.load_state_dict(model.state_dict())
38
+
39
+ model_named_params = dict(model.named_parameters())
40
+ model_copy_named_params = dict(model_copy.named_parameters())
41
+
42
+ norm_factor = 1024**3
43
+
44
+ t_before = torch.cuda.get_device_properties(0).total_memory / norm_factor
45
+ r_before = torch.cuda.memory_reserved(0) / norm_factor
46
+ a_before = torch.cuda.memory_allocated(0) / norm_factor
47
+
48
+ print(f'Before Total memory: {t_before} GB, reserved: {r_before} GB, allocated: {a_before} GB')
49
+
50
+ model_wrapper = MemoryBufferModuleWrapper(model)
51
+
52
+ t = torch.cuda.get_device_properties(0).total_memory / norm_factor
53
+ r = torch.cuda.memory_reserved(0) / norm_factor
54
+ a = torch.cuda.memory_allocated(0) / norm_factor
55
+
56
+ gc.collect()
57
+ torch.cuda.empty_cache()
58
+
59
+ print(f'After Total memory: {t} GB, reserved: {r} GB, allocated: {a} GB')
60
+
61
+ change_ratio = (a - a_before) / a_before
62
+ assert change_ratio < 0.01, f'make sure the allocated change is less than 1%, Got {change_ratio}'
63
+
64
+ for (name1, param1), (name2, param2) in zip(model.named_parameters(), model_copy.named_parameters()):
65
+ assert name1 == name2
66
+ assert torch.eq(param1.data, param2.data).all(), f'{param1.data}, {param2.data}, {name1}'
67
+
68
+
69
+ if __name__ == '__main__':
70
+ test_memory_buffers()
tests/gpu_utility/test_ops.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
+
16
+ def test_flash_attn_cross_entropy():
17
+ from verl.utils.torch_functional import logprobs_from_logits_naive
18
+
19
+ from verl.utils.debug import log_gpu_memory_usage
20
+
21
+ from flash_attn.ops.triton.cross_entropy import cross_entropy_loss
22
+
23
+ import torch
24
+ from torch import nn
25
+
26
+ log_gpu_memory_usage('At start')
27
+
28
+ hidden_states = torch.randn(size=(2048, 5120), device='cuda', requires_grad=True, dtype=torch.bfloat16)
29
+
30
+ linear = nn.Linear(in_features=5120, out_features=155136, bias=False, device='cuda', dtype=torch.bfloat16)
31
+
32
+ logits = linear(hidden_states)
33
+
34
+ # logits = logits.float()
35
+ labels = torch.randint(low=0, high=155136, size=(2048,), device='cuda')
36
+
37
+ log_gpu_memory_usage('before computation')
38
+ # output = checkpoint.checkpoint(logprobs_from_logits, logits, labels, use_reentrant=True)
39
+ output = -cross_entropy_loss(logits, labels)[0]
40
+ # output = logprobs_from_logits(logits, labels)
41
+ log_gpu_memory_usage('After forward')
42
+ output.sum().backward()
43
+ log_gpu_memory_usage('After backward')
44
+
45
+ groundtruth = logprobs_from_logits_naive(logits.float(), labels)
46
+
47
+ torch.testing.assert_close(output, groundtruth)
tests/gpu_utility/test_torch_functional.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 verl.utils.model import create_random_mask
16
+ from flash_attn.bert_padding import unpad_input
17
+ import torch
18
+
19
+
20
+ def test_log_probs_from_logits_response_rmpad():
21
+ from verl.utils.torch_functional import log_probs_from_logits_response, log_probs_from_logits_response_rmpad
22
+ vocab_size = 32000
23
+ batch_size = 2
24
+ prompt_length = 256
25
+ response_length = 256
26
+
27
+ input_ids = torch.randint(low=0, high=vocab_size, size=(batch_size, prompt_length + response_length), device='cuda')
28
+ attention_mask = create_random_mask(input_ids=input_ids,
29
+ max_ratio_of_left_padding=0.2,
30
+ max_ratio_of_valid_token=0.8,
31
+ min_ratio_of_valid_token=0.6)
32
+
33
+ response_mask = attention_mask[:, -response_length:]
34
+
35
+ assert torch.all(response_mask[:, 0] == 1)
36
+
37
+ logits = torch.randn(batch_size, prompt_length + response_length, vocab_size, device='cuda')
38
+ logits_rmpad = unpad_input(logits, attention_mask)[0]
39
+
40
+ expected_output = log_probs_from_logits_response(input_ids=input_ids,
41
+ logits=logits,
42
+ response_length=response_length)
43
+ actual_output = log_probs_from_logits_response_rmpad(input_ids=input_ids,
44
+ attention_mask=attention_mask,
45
+ logits_rmpad=logits_rmpad,
46
+ response_length=response_length)
47
+
48
+ # This should bitwise align as only this operation only contains gather operators
49
+ assert torch.all(torch.eq(actual_output * response_mask, expected_output * response_mask))
50
+
51
+
52
+ def test_lr_scheduler():
53
+ from torch import nn
54
+ model = nn.Linear(10, 10)
55
+ optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
56
+
57
+ from verl.utils.torch_functional import get_constant_schedule_with_warmup
58
+ constant_lr = get_constant_schedule_with_warmup(optimizer=optimizer, num_warmup_steps=2)
59
+
60
+ lr_lst = []
61
+
62
+ for _ in range(5):
63
+ lr_lst.append(constant_lr.get_last_lr()[0])
64
+ constant_lr.step()
65
+
66
+ torch.testing.assert_close(lr_lst, [0.0, 0.0005, 0.001, 0.001, 0.001])
67
+
68
+ from verl.utils.torch_functional import get_cosine_schedule_with_warmup
69
+ optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
70
+ cosine_lr = get_cosine_schedule_with_warmup(optimizer=optimizer,
71
+ num_warmup_steps=2,
72
+ num_training_steps=5,
73
+ min_lr_ratio=0.1)
74
+
75
+ lr_lst = []
76
+
77
+ for _ in range(5):
78
+ lr_lst.append(cosine_lr.get_last_lr()[0])
79
+ cosine_lr.step()
80
+
81
+ torch.testing.assert_close(lr_lst, [0.0, 0.0005, 0.001, 0.0007750000000000002, 0.0003250000000000002])
tests/ray/test_data_transfer.py CHANGED
@@ -52,7 +52,7 @@ class DummyWorker(Worker):
52
  def test_data_transfer():
53
  ray.init()
54
  # construct resource pool
55
- resource_pool = RayResourcePool([2])
56
  cls_with_init = RayClassWithInitArgs(cls=DummyWorker)
57
  # construct worker group
58
  wg = RayWorkerGroup(resource_pool, cls_with_init)
 
52
  def test_data_transfer():
53
  ray.init()
54
  # construct resource pool
55
+ resource_pool = RayResourcePool([8])
56
  cls_with_init = RayClassWithInitArgs(cls=DummyWorker)
57
  # construct worker group
58
  wg = RayWorkerGroup(resource_pool, cls_with_init)
tests/ray/test_remote_api.py DELETED
@@ -1,89 +0,0 @@
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 verl.single_controller.remote import remote, RemoteBackend, SharedResourcePool
16
- from verl.single_controller.base.decorator import register, Dispatch
17
- from verl.single_controller.base.worker import Worker
18
-
19
-
20
- @remote(process_on_nodes=[3], use_gpu=True, name_prefix="actor", sharing=SharedResourcePool)
21
- class Actor(Worker):
22
- ...
23
-
24
-
25
- @remote(process_on_nodes=[3], use_gpu=True, name_prefix="critic", sharing=SharedResourcePool)
26
- class Critic(Worker):
27
- ...
28
-
29
-
30
- @remote(process_on_nodes=[2], use_gpu=True, name_prefix="reward", sharing=SharedResourcePool.from_role("actor"))
31
- class Reward(Worker):
32
- ...
33
-
34
-
35
- @remote(process_on_nodes=[2], use_gpu=True, name_prefix="ref", sharing=SharedResourcePool.from_role("actor", "critic"))
36
- class Ref(Worker):
37
- ...
38
-
39
-
40
- @remote(process_on_nodes=[1], use_gpu=True, name_prefix="sec_rm", sharing=SharedResourcePool.from_role("any"))
41
- class SecRM(Worker):
42
- ...
43
-
44
-
45
- def test():
46
- print("Remote.init_distributed")
47
- remote.init_distributed(backend=RemoteBackend.RAY)
48
-
49
- print("create actor worker group")
50
- actor = Actor()
51
-
52
- print("create critic worker group")
53
- critic = Critic()
54
-
55
- print("create rm worker group")
56
- reward = Reward()
57
-
58
- print("create ref worker group")
59
- ref = Ref()
60
-
61
- print("create sec_rm worker group")
62
- sec_rm = SecRM()
63
-
64
- actor_gpus = actor.execute_all_sync("get_cuda_visible_devices")
65
- critic_gpus = critic.execute_all_sync("get_cuda_visible_devices")
66
- reward_gpus = reward.execute_all_sync("get_cuda_visible_devices")
67
- ref_gpus = ref.execute_all_sync("get_cuda_visible_devices")
68
- sec_rm_gpus = sec_rm.execute_all_sync("get_cuda_visible_devices")
69
-
70
- for gpu in actor_gpus:
71
- assert gpu not in critic_gpus, f"actor gpus = {actor_gpus}, critic gpus = {critic_gpus}"
72
-
73
- for gpu in critic_gpus:
74
- assert gpu not in actor_gpus, f"actor gpus = {actor_gpus}, critic gpus = {critic_gpus}"
75
-
76
- for gpu in reward_gpus:
77
- assert gpu in actor_gpus, f"actor gpus = {actor_gpus}, reward gpus = {reward_gpus}"
78
-
79
- for gpu in ref_gpus:
80
- assert gpu in actor_gpus + critic_gpus, \
81
- f"actor gpus = {actor_gpus}, critic gpus = {critic_gpus}, ref gpus = {ref_gpus}"
82
-
83
- for gpu in sec_rm_gpus:
84
- assert gpu in actor_gpus + critic_gpus, \
85
- f"actor gpus = {actor_gpus}, critic gpus = {critic_gpus}, sec rm gpus = {sec_rm_gpus}"
86
-
87
- # for ci only
88
- import ray
89
- ray.shutdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/rollout/run_fsdp_vllm.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM
17
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP, ShardingStrategy, MixedPrecision, CPUOffload
18
+ from torch.distributed.fsdp.api import ShardingStrategy, ShardedStateDictConfig, StateDictType
19
+ import torch
20
+
21
+ from verl.utils.distributed import initialize_global_process_group
22
+ from verl.third_party.vllm import LLM
23
+
24
+ from vllm import SamplingParams
25
+
26
+
27
+ def main():
28
+ assert torch.cuda.is_available(), 'CUDA must be present to run FSDP vLLM example'
29
+ local_rank, rank, world_size = initialize_global_process_group()
30
+
31
+ local_cache_path = '~/.cache/verl/rlhf'
32
+ local_cache_path = os.path.expanduser(local_cache_path)
33
+ hdfs_path = 'Qwen/Qwen2-7B-Instruct'
34
+
35
+ from verl.utils.fs import copy_local_path_from_hdfs
36
+ local_model_path = copy_local_path_from_hdfs(src=hdfs_path, cache_dir=local_cache_path)
37
+ tokenizer = AutoTokenizer.from_pretrained(local_model_path, trust_remote_code=True)
38
+ actor_model_config = AutoConfig.from_pretrained(local_model_path, trust_remote_code=True)
39
+ with torch.device("cuda"):
40
+ actor_model = AutoModelForCausalLM.from_pretrained(local_model_path, trust_remote_code=True)
41
+ actor_model.to(torch.bfloat16)
42
+
43
+ max_prompt_length = 16
44
+ response_length = 32
45
+ preencode_prompts = [
46
+ "The president of the United States is",
47
+ "The capital of France is",
48
+ "The future of AI is",
49
+ ]
50
+ tokenizer.pad_token = tokenizer.eos_token
51
+ prompts = tokenizer(preencode_prompts, return_tensors='pt', padding=True)
52
+ input_ids = prompts['input_ids']
53
+ attention_mask = prompts['attention_mask']
54
+ from verl.utils.torch_functional import pad_sequence_to_length
55
+ input_ids = pad_sequence_to_length(input_ids, max_prompt_length, tokenizer.pad_token_id, left_pad=True).cuda()
56
+ attention_mask = pad_sequence_to_length(attention_mask, max_prompt_length, 0, left_pad=True).cuda()
57
+
58
+ from transformers import GenerationConfig
59
+ generation_config = GenerationConfig(do_sample=False)
60
+ actor_model.cuda()
61
+ output = actor_model.generate(
62
+ input_ids=input_ids,
63
+ attention_mask=attention_mask,
64
+ max_new_tokens=32,
65
+ # max_length=max_length,
66
+ eos_token_id=tokenizer.eos_token_id,
67
+ pad_token_id=tokenizer.pad_token_id,
68
+ generation_config=generation_config,
69
+ # renormalize_logits=True,
70
+ output_scores=False, # this is potentially very large
71
+ return_dict_in_generate=True,
72
+ use_cache=False) # may OOM when use_cache = True
73
+ seq = output.sequences
74
+ response = seq[:, max_prompt_length:]
75
+
76
+ print(f'hf response: {tokenizer.batch_decode(response)}')
77
+
78
+ tensor_model_parallel_size = 4
79
+ from torch.distributed.device_mesh import init_device_mesh
80
+ device_mesh = init_device_mesh('cuda', mesh_shape=(world_size,), mesh_dim_names=['fsdp'])
81
+
82
+ mixed_precision = MixedPrecision(param_dtype=torch.bfloat16, reduce_dtype=torch.float32, buffer_dtype=torch.float32)
83
+ fsdp_model = FSDP(actor_model,
84
+ use_orig_params=True,
85
+ auto_wrap_policy=None,
86
+ device_id=torch.cuda.current_device(),
87
+ sharding_strategy=ShardingStrategy.FULL_SHARD,
88
+ mixed_precision=mixed_precision,
89
+ cpu_offload=CPUOffload(offload_params=False),
90
+ sync_module_states=False,
91
+ device_mesh=device_mesh)
92
+
93
+ FSDP.set_state_dict_type(fsdp_model,
94
+ state_dict_type=StateDictType.SHARDED_STATE_DICT,
95
+ state_dict_config=ShardedStateDictConfig())
96
+
97
+ state_dict = fsdp_model.state_dict()
98
+
99
+ sampling_params = SamplingParams(temperature=0,
100
+ top_p=1,
101
+ n=1,
102
+ max_tokens=response_length,
103
+ logprobs=1,
104
+ ignore_eos=True,
105
+ detokenize=False)
106
+
107
+ print(actor_model_config)
108
+ llm = LLM(model=None,
109
+ tokenizer=tokenizer,
110
+ model_hf_config=actor_model_config,
111
+ tensor_parallel_size=tensor_model_parallel_size,
112
+ enforce_eager=True,
113
+ dtype='bfloat16',
114
+ load_format='dummy_dtensor',
115
+ gpu_memory_utilization=0.1,
116
+ trust_remote_code=True)
117
+
118
+ llm.sync_model_weights(actor_weights=state_dict, load_format='dtensor')
119
+
120
+ input_ids = input_ids.cuda()
121
+ attention_mask = attention_mask.cuda()
122
+ idx_list = []
123
+ batch_size = input_ids.shape[0]
124
+
125
+ pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
126
+ from verl.workers.rollout.vllm_rollout.vllm_rollout import _pre_process_inputs
127
+ for i in range(batch_size):
128
+ idx_list.append(_pre_process_inputs(pad_token_id, input_ids[i]))
129
+ print('start generation')
130
+ outputs = llm.generate(prompt_token_ids=idx_list, sampling_params=sampling_params, use_tqdm=False)
131
+ vllm_output = outputs[0].cuda()
132
+ if torch.distributed.get_rank() == 0:
133
+ print(f'hf response: {tokenizer.batch_decode(response)}')
134
+ print(f'vllm response: {tokenizer.batch_decode(vllm_output)}')
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
tests/rollout/test_vllm_hf_loader.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 torch
17
+ import transformers
18
+
19
+ from verl.third_party.vllm import LLM, vllm_version
20
+ from verl.utils.model import update_model_config
21
+ from vllm import SamplingParams
22
+ from transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM
23
+
24
+ from transformers import GenerationConfig
25
+
26
+ from verl.utils.torch_functional import pad_sequence_to_length
27
+ from verl.workers.rollout.vllm_rollout.vllm_rollout import _pre_process_inputs
28
+
29
+
30
+ def levenshtein(s1, s2):
31
+ m, n = len(s1), len(s2)
32
+ # Initialize matrix of zeros
33
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
34
+ # Initialize first column and first row of the matrix
35
+ for i in range(m + 1):
36
+ dp[i][0] = i # Deletion from s1 to empty string
37
+ for j in range(n + 1):
38
+ dp[0][j] = j # Insertion to s1 from empty string
39
+ # Compute the Levenshtein distance matrix
40
+ for i in range(1, m + 1):
41
+ for j in range(1, n + 1):
42
+ cost = 0 if s1[i - 1] == s2[j - 1] else 1 # No cost if characters match
43
+ dp[i][j] = min(
44
+ dp[i - 1][j] + 1, # Deletion
45
+ dp[i][j - 1] + 1, # Insertion
46
+ dp[i - 1][j - 1] + cost # Substitution
47
+ )
48
+ return dp[m][n]
49
+
50
+
51
+ def are_lists_similar(a, b):
52
+ if len(a) != len(b):
53
+ print("The lists are of different lengths.")
54
+ return False
55
+
56
+ total_length = 0
57
+ total_diff = 0
58
+
59
+ for s1, s2 in zip(a, b):
60
+ max_len = max(len(s1), len(s2))
61
+ total_length += max_len
62
+ diff = levenshtein(s1, s2)
63
+ total_diff += diff
64
+ print(f"Comparing strings:\n{s1}\n{s2}\nDifference: {diff} characters\n")
65
+
66
+ percentage_difference = (total_diff / total_length) * 100
67
+ print(f"Total difference: {percentage_difference:.2f}%")
68
+
69
+ return percentage_difference <= 10
70
+
71
+
72
+ def test_vllm_with_hf():
73
+ assert torch.cuda.device_count() >= 2, 'At least 2 GPUs is required to run tp+dp tests.'
74
+
75
+ # fill rollout config
76
+ max_prompt_length = 16
77
+ max_response_length = 16
78
+
79
+ # Initialize model and token
80
+ local_cache_path = '~/.cache/verl/rlhf'
81
+ local_cache_path = os.path.expanduser(local_cache_path)
82
+ hdfs_path = 'deepseek-ai/deepseek-llm-7b-chat'
83
+ from verl.utils.fs import copy_local_path_from_hdfs
84
+ local_model_path = copy_local_path_from_hdfs(src=hdfs_path, cache_dir=local_cache_path)
85
+ tokenizer = AutoTokenizer.from_pretrained(local_model_path)
86
+
87
+ preencode_prompts = [
88
+ "Who won the Champions League in 2019?",
89
+ "The founder of Apple is",
90
+ "What's your name",
91
+ ]
92
+ tokenizer.pad_token = tokenizer.eos_token
93
+ prompts = tokenizer(preencode_prompts, return_tensors='pt', padding=True)
94
+ input_ids = prompts['input_ids']
95
+ attention_mask = prompts['attention_mask']
96
+
97
+ input_ids = pad_sequence_to_length(input_ids, max_prompt_length, tokenizer.pad_token_id, left_pad=True)
98
+ attention_mask = pad_sequence_to_length(attention_mask, max_prompt_length, 0, left_pad=True)
99
+
100
+ actor_model = AutoModelForCausalLM.from_pretrained(local_model_path)
101
+ actor_model.to(torch.bfloat16)
102
+
103
+ actor_model_config = AutoConfig.from_pretrained(local_model_path)
104
+
105
+ temperature = 0
106
+ top_p = 1
107
+
108
+ kwargs = dict(n=1,
109
+ temperature=temperature,
110
+ top_p=top_p,
111
+ max_tokens=max_response_length,
112
+ logprobs=1,
113
+ ignore_eos=True)
114
+
115
+ if vllm_version in ('0.4.2', '0.5.4', '0.6.3'):
116
+ kwargs['detokenize'] = False
117
+ sampling_params = SamplingParams(**kwargs)
118
+
119
+ tensor_parallel_size = 2
120
+
121
+ llm = LLM(model=actor_model,
122
+ tokenizer=tokenizer,
123
+ model_hf_config=actor_model_config,
124
+ tensor_parallel_size=tensor_parallel_size,
125
+ dtype='bfloat16',
126
+ gpu_memory_utilization=0.1,
127
+ load_format='hf')
128
+
129
+ print('start generation')
130
+ input_ids = input_ids.cuda()
131
+ attention_mask = attention_mask.cuda()
132
+ batch_size = input_ids.size(0)
133
+
134
+ idx_list = []
135
+ # parse idx from torch.Tensor to List[List[str]]
136
+ for i in range(batch_size):
137
+ idx_list.append(_pre_process_inputs(tokenizer.pad_token_id, input_ids[i]))
138
+ outputs = llm.generate(prompt_token_ids=idx_list, sampling_params=sampling_params, use_tqdm=False)
139
+ vllm_output = outputs[0].cuda()
140
+ llm.free_cache_engine()
141
+ llm = None
142
+ import gc
143
+ torch.cuda.empty_cache()
144
+ gc.collect()
145
+
146
+ generation_config = GenerationConfig(do_sample=False)
147
+ actor_model.cuda()
148
+ output = actor_model.generate(
149
+ input_ids=input_ids,
150
+ attention_mask=attention_mask,
151
+ max_new_tokens=max_response_length,
152
+ # max_length=max_length,
153
+ eos_token_id=tokenizer.eos_token_id,
154
+ pad_token_id=tokenizer.pad_token_id,
155
+ generation_config=generation_config,
156
+ # renormalize_logits=True,
157
+ output_scores=False, # this is potentially very large
158
+ return_dict_in_generate=True,
159
+ use_cache=False) # may OOM when use_cache = True
160
+ seq = output.sequences
161
+ response = seq[:, max_prompt_length:]
162
+
163
+ hf_response_tokens = tokenizer.batch_decode(response)
164
+ vllm_response_tokens = tokenizer.batch_decode(vllm_output)
165
+
166
+ print(f'hf response: {hf_response_tokens}')
167
+ print(f'vllm response: {vllm_response_tokens}')
168
+ assert are_lists_similar(hf_response_tokens, vllm_response_tokens), \
169
+ f'Strings differ more than 10%:\n'
170
+ print('Check Pass')
171
+
172
+
173
+ # if __name__ == "__main__":
174
+ # test_vllm_with_hf()
tests/sanity/test_import.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ def test_import():
17
+ import verl
18
+ print(verl.__version__)
19
+
20
+
21
+ def test_single_controller_import():
22
+ import verl.single_controller
23
+ print(verl.single_controller.__version__)
verl/single_controller/base/worker.py CHANGED
@@ -17,7 +17,7 @@ the class for Worker
17
  import os
18
  import socket
19
  from dataclasses import dataclass
20
- from verl.single_controller.base.decorator import register, Dispatch
21
 
22
 
23
  @dataclass
@@ -179,3 +179,8 @@ class Worker(WorkerHelper):
179
  def execute_with_func_generator(self, func, *args, **kwargs):
180
  ret_proto = func(self, *args, **kwargs)
181
  return ret_proto
 
 
 
 
 
 
17
  import os
18
  import socket
19
  from dataclasses import dataclass
20
+ from verl.single_controller.base.decorator import register, Dispatch, Execute
21
 
22
 
23
  @dataclass
 
179
  def execute_with_func_generator(self, func, *args, **kwargs):
180
  ret_proto = func(self, *args, **kwargs)
181
  return ret_proto
182
+
183
+ @register(dispatch_mode=Dispatch.ALL_TO_ALL, execute_mode=Execute.RANK_ZERO)
184
+ def execute_func_rank_zero(self, func, *args, **kwargs):
185
+ result = func(*args, **kwargs)
186
+ return result
verl/third_party/vllm/vllm_v_0_6_3/model_loader.py CHANGED
@@ -196,6 +196,9 @@ class HFLoader(BaseModelLoader):
196
  raise ValueError(f"Model loader extra config is not supported for "
197
  f"load format {load_config.load_format}")
198
 
 
 
 
199
  def _get_weights_iterator(self, actor_model: Union[PreTrainedModel, Dict]):
200
  if isinstance(actor_model, Dict):
201
  return actor_model.items()
@@ -241,6 +244,9 @@ class DTensorLoader(BaseModelLoader):
241
  raise ValueError(f"Model loader extra config is not supported for "
242
  f"load format {load_config.load_format}")
243
 
 
 
 
244
  def _get_weights_iterator(actor_model: Union[PreTrainedModel, Dict]):
245
  # NOTE(shengguangming) Load the weights from the actor model
246
  pass
 
196
  raise ValueError(f"Model loader extra config is not supported for "
197
  f"load format {load_config.load_format}")
198
 
199
+ def download_model(self, model_config: ModelConfig) -> None:
200
+ pass # Nothing to download
201
+
202
  def _get_weights_iterator(self, actor_model: Union[PreTrainedModel, Dict]):
203
  if isinstance(actor_model, Dict):
204
  return actor_model.items()
 
244
  raise ValueError(f"Model loader extra config is not supported for "
245
  f"load format {load_config.load_format}")
246
 
247
+ def download_model(self, model_config: ModelConfig) -> None:
248
+ pass # Nothing to download
249
+
250
  def _get_weights_iterator(actor_model: Union[PreTrainedModel, Dict]):
251
  # NOTE(shengguangming) Load the weights from the actor model
252
  pass
verl/trainer/ppo/ray_trainer.py CHANGED
@@ -335,7 +335,7 @@ class RayPPOTrainer(object):
335
  # test_batch = test_batch.to('cuda')
336
 
337
  # we only do validation on rule-based rm
338
- if test_batch[0].non_tensor_batch['reward_model']['style'] == 'model':
339
  return {}
340
 
341
  test_gen_batch = test_batch.pop(['input_ids', 'attention_mask', 'position_ids'])
 
335
  # test_batch = test_batch.to('cuda')
336
 
337
  # we only do validation on rule-based rm
338
+ if self.config.reward_model.enable and test_batch[0].non_tensor_batch['reward_model']['style'] == 'model':
339
  return {}
340
 
341
  test_gen_batch = test_batch.pop(['input_ids', 'attention_mask', 'position_ids'])