-
Notifications
You must be signed in to change notification settings - Fork 89
Expand file tree
/
Copy pathray_trainer.py
More file actions
260 lines (227 loc) · 12.8 KB
/
Copy pathray_trainer.py
File metadata and controls
260 lines (227 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import ray
import uuid
import torch
import os
import json
import numpy as np
from copy import deepcopy
from collections import defaultdict
from typing import Optional
from verl.trainer.ppo.ray_trainer import (
RayPPOTrainer,
pad_dataproto_to_divisor,
unpad_dataproto,
process_validation_metrics,
) # for train and validate
from verl.trainer.ppo.ray_trainer import (
DataProto,
) # for init
from verl.utils.debug import marked_timer
##############################################################################
#### Replace the original classes/functions with verl-tool customized ones ####
import verl.experimental.agent_loop
from verl_tool.agent_loop import AgentLoopManager
import verl.trainer.ppo.ray_trainer
from .reward import compute_reward, compute_reward_async
from verl_tool.workers.rollout.vllm_rollout.vllm_async_server import VerlToolvLLMHttpServer
import verl.workers.rollout.vllm_rollout.vllm_async_server
from .metric_util import compute_data_metrics, process_validation_metrics
verl.experimental.agent_loop.AgentLoopManager = AgentLoopManager
verl.trainer.ppo.ray_trainer.compute_reward = compute_reward
verl.trainer.ppo.ray_trainer.compute_reward_async = compute_reward_async
verl.trainer.ppo.ray_trainer.compute_data_metrics = compute_data_metrics
verl.trainer.ppo.ray_trainer.process_validation_metrics = process_validation_metrics
verl.workers.rollout.vllm_rollout.vllm_async_server.vLLMHttpServer = VerlToolvLLMHttpServer
##############################################################################
class AgentRayPPOTrainer(RayPPOTrainer):
def _validate(self):
data_source_lst = []
reward_extra_infos_dict: dict[str, list] = defaultdict(list)
# Lists to collect samples for the table
sample_inputs = []
sample_outputs = []
sample_gts = []
sample_scores = []
sample_turns = []
sample_uids = []
for test_data in self.val_dataloader:
test_batch = DataProto.from_single_dict(test_data)
if "uid" not in test_batch.non_tensor_batch:
test_batch.non_tensor_batch["uid"] = np.array(
[str(uuid.uuid4()) for _ in range(len(test_batch.batch))], dtype=object
)
# repeat test batch
test_batch = test_batch.repeat(
repeat_times=self.config.actor_rollout_ref.rollout.val_kwargs.n, interleave=True
)
# we only do validation on rule-based rm
if self.config.reward_model.enable and test_batch[0].non_tensor_batch["reward_model"]["style"] == "model":
return {}
# Store original inputs
input_ids = test_batch.batch["input_ids"]
# TODO: Can we keep special tokens except for padding tokens?
input_texts = [self.tokenizer.decode(ids, skip_special_tokens=True) for ids in input_ids]
sample_inputs.extend(input_texts)
sample_uids.extend(test_batch.non_tensor_batch["uid"])
ground_truths = [
item.non_tensor_batch.get("reward_model", {}).get("ground_truth", None) for item in test_batch
]
sample_gts.extend(ground_truths)
test_gen_batch = self._get_gen_batch(test_batch)
test_gen_batch.meta_info = {
"eos_token_id": self.tokenizer.eos_token_id,
"pad_token_id": self.tokenizer.pad_token_id,
"recompute_log_prob": False,
"do_sample": self.config.actor_rollout_ref.rollout.val_kwargs.do_sample,
"validate": True,
"global_steps": self.global_steps,
}
print(f"test_gen_batch meta info: {test_gen_batch.meta_info}")
# pad to be divisible by dp_size
size_divisor = (
self.actor_rollout_wg.world_size
if not self.async_rollout_mode
else self.config.actor_rollout_ref.rollout.agent.num_workers
)
test_gen_batch_padded, pad_size = pad_dataproto_to_divisor(test_gen_batch, size_divisor)
if not self.async_rollout_mode:
test_output_gen_batch_padded = self.actor_rollout_wg.generate_sequences(test_gen_batch_padded)
else:
test_output_gen_batch_padded = self.async_rollout_manager.generate_sequences(test_gen_batch_padded)
# unpad
test_output_gen_batch = unpad_dataproto(test_output_gen_batch_padded, pad_size=pad_size)
print("validation generation end")
# Store generated outputs
output_ids = test_output_gen_batch.batch["responses"]
output_attention_mask = test_output_gen_batch.batch["attention_mask"][:, test_output_gen_batch.batch["prompts"].shape[1]:]
output_texts = [self.tokenizer.decode(ids[output_attention_mask[i]==1], skip_special_tokens=False) for i, ids in enumerate(output_ids)]
sample_outputs.extend(output_texts)
test_batch = test_batch.union(test_output_gen_batch)
test_batch.meta_info["validate"] = True
# evaluate using reward_function
if self.val_reward_fn is None:
raise ValueError("val_reward_fn must be provided for validation.")
result = self.val_reward_fn(test_batch, return_dict=True)
reward_tensor = result["reward_tensor"]
scores = reward_tensor.sum(-1).cpu().tolist()
sample_scores.extend(scores)
reward_extra_infos_dict["reward"].extend(scores)
if "reward_extra_info" in result:
for key, lst in result["reward_extra_info"].items():
reward_extra_infos_dict[key].extend(lst)
tool_interact_info = test_batch.non_tensor_batch.get('tool_interact_info', None)
if isinstance(tool_interact_info, np.ndarray):
tool_interact_info = tool_interact_info.tolist()
if tool_interact_info:
for tool_interact in tool_interact_info:
if "image" in tool_interact:
if isinstance(tool_interact['image'], list):
tool_interact['image'] = [x[:50] for x in tool_interact['image']] # crop the image to first 50 characters
elif isinstance(tool_interact['image'], str):
tool_interact['image'] = tool_interact['image'][:50] # for debug
if "tool_interact_info" not in reward_extra_infos_dict:
reward_extra_infos_dict["tool_interact_info"] = []
if "traj_stop_reason" not in reward_extra_infos_dict:
reward_extra_infos_dict["traj_stop_reason"] = []
reward_extra_infos_dict["tool_interact_info"].extend(tool_interact_info)
reward_extra_infos_dict["traj_stop_reason"].extend(
test_batch.non_tensor_batch.get("traj_stop_reason", [None] * reward_tensor.shape[0])
)
reward_extra_infos_dict["verl_tool_metrics"].extend(
test_batch.non_tensor_batch.get("verl_tool_metrics", [None] * reward_tensor.shape[0])
)
# collect num_turns of each prompt
if "__num_turns__" in test_batch.non_tensor_batch:
sample_turns.append(test_batch.non_tensor_batch["__num_turns__"])
data_source_lst.append(test_batch.non_tensor_batch.get("data_source", ["unknown"] * reward_tensor.shape[0]))
self._maybe_log_val_generations(inputs=sample_inputs, outputs=sample_outputs, scores=sample_scores)
# dump generations
val_data_dir = self.config.trainer.get("validation_data_dir", None)
if val_data_dir:
self._dump_generations(
inputs=sample_inputs,
outputs=sample_outputs,
gts=sample_gts,
scores=sample_scores,
reward_extra_infos_dict=reward_extra_infos_dict,
dump_path=val_data_dir,
)
if "tool_interact_info" in reward_extra_infos_dict:
# remove if after dump
reward_extra_infos_dict.pop("tool_interact_info")
if "traj_stop_reason" in reward_extra_infos_dict:
reward_extra_infos_dict.pop("traj_stop_reason")
if "verl_tool_metrics" in reward_extra_infos_dict:
reward_extra_infos_dict.pop("verl_tool_metrics")
for key_info, lst in reward_extra_infos_dict.items():
assert len(lst) == 0 or len(lst) == len(sample_scores), f"{key_info}: {len(lst)=}, {len(sample_scores)=}"
data_sources = np.concatenate(data_source_lst, axis=0)
data_src2var2metric2val = process_validation_metrics(data_sources, sample_uids, reward_extra_infos_dict)
metric_dict = {}
for data_source, var2metric2val in data_src2var2metric2val.items():
core_var = "acc" if "acc" in var2metric2val else "reward"
for var_name, metric2val in var2metric2val.items():
n_max = max([int(name.split("@")[-1].split("/")[0]) for name in metric2val.keys()])
for metric_name, metric_val in metric2val.items():
if (
(var_name == core_var)
and any(metric_name.startswith(pfx) for pfx in ["mean", "maj", "best"])
and (f"@{n_max}" in metric_name)
):
metric_sec = "val-core"
else:
metric_sec = "val-aux"
pfx = f"{metric_sec}/{data_source}/{var_name}/{metric_name}"
metric_dict[pfx] = metric_val
if len(sample_turns) > 0:
sample_turns = np.concatenate(sample_turns)
metric_dict["val-aux/num_turns/min"] = sample_turns.min()
metric_dict["val-aux/num_turns/max"] = sample_turns.max()
metric_dict["val-aux/num_turns/mean"] = sample_turns.mean()
return metric_dict
def _log_rollout_data(
self, batch: DataProto, reward_extra_infos_dict: dict, timing_raw: dict, rollout_data_dir: str
):
"""Log rollout data to disk.
Args:
batch (DataProto): The batch containing rollout data
reward_extra_infos_dict (dict): Additional reward information to log
timing_raw (dict): Timing information for profiling
rollout_data_dir (str): Directory path to save the rollout data
"""
with marked_timer("dump_rollout_generations", timing_raw, color="green"):
inputs_attention_masks = batch.batch['attention_mask'][:, :batch.batch['prompts'].shape[1]]
outputs_attention_masks = batch.batch['attention_mask'][:, batch.batch['prompts'].shape[1]:]
inputs = [self.tokenizer.decode(batch.batch["prompts"][i][inputs_attention_masks[i]==1], skip_special_tokens=False) for i in range(batch.batch["prompts"].shape[0])]
outputs = [self.tokenizer.decode(batch.batch["responses"][i][outputs_attention_masks[i]==1], skip_special_tokens=False) for i in range(batch.batch["responses"].shape[0])]
scores = batch.batch["token_level_scores"].sum(-1).cpu().tolist()
sample_gts = [item.non_tensor_batch.get("reward_model", {}).get("ground_truth", None) for item in batch]
reward_extra_infos_to_dump = reward_extra_infos_dict.copy()
if "request_id" in batch.non_tensor_batch:
reward_extra_infos_dict.setdefault(
"request_id",
batch.non_tensor_batch["request_id"].tolist(),
)
tool_interact_info = batch.non_tensor_batch.get('tool_interact_info', None)
if isinstance(tool_interact_info, np.ndarray):
tool_interact_info = tool_interact_info.tolist()
if tool_interact_info:
for tool_interact in tool_interact_info:
if "image" in tool_interact:
if isinstance(tool_interact['image'], list):
tool_interact['image'] = [x[:50] for x in tool_interact['image']] # crop the image to first 50 characters
elif isinstance(tool_interact['image'], str):
tool_interact['image'] = tool_interact['image'][:50] # for debug
reward_extra_infos_to_dump.update({
"tool_interact_info": tool_interact_info,
"traj_stop_reason": batch.non_tensor_batch.get("traj_stop_reason", None),
"verl_tool_metrics": batch.non_tensor_batch.get("verl_tool_metrics", None),
})
self._dump_generations(
inputs=inputs,
outputs=outputs,
gts=sample_gts,
scores=scores,
reward_extra_infos_dict=reward_extra_infos_to_dump,
dump_path=rollout_data_dir,
)