Skip to content

Commit f5f4048

Browse files
committed
Merge branch 'agentjet_codebase_backup' into HEAD
2 parents 83d9ef4 + bd9041c commit f5f4048

34 files changed

Lines changed: 1125 additions & 598 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,9 @@ cocktail_results_*
191191
PRINCIPLE.md
192192
TODO.md
193193
plot_mean_reward.py
194+
appworld_swarm_results_token
195+
appworld_swarm_results_text
196+
appworld_swarm_results
197+
TODO
198+
.trash.*
199+
tutorial/opencode_build_aime/auto_research/results/*

ajet/backbone/main_verl.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ def run_ppo(config: DictConfig) -> None:
5959
if not ray.is_initialized():
6060
# this is for local ray cluster
6161
runtime_env = get_runtime_env(config)
62-
ray.init(
63-
runtime_env=runtime_env,
64-
)
62+
ray.init(runtime_env=runtime_env)
6563

6664
def on_shutdown():
6765
if ray.is_initialized():

ajet/backbone/trainer_verl.py

Lines changed: 169 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,6 @@ def union_gen_batch_via_task_id(tasks, batch: DataProto, gen_batch_output: DataP
131131
task_id_counter[tid] += 1
132132
else:
133133
task_id_counter[tid] = 1
134-
current_id = task_id_counter[tid]
135-
gen_batch_output.non_tensor_batch['rollout_ids'][i] = f"T{tid}R{current_id}"
136134
logger.info(f'task_id_counter: {task_id_counter}')
137135
return gen_batch_output
138136

@@ -166,6 +164,117 @@ def import_or_export_data_proto(batch: DataProto, direction: str = "export", fil
166164
else:
167165
raise ValueError(f"direction must be 'import' or 'export', got '{direction}'")
168166

167+
def compute_grpo_episode_level_outcome_advantage(
168+
token_level_rewards: torch.Tensor,
169+
response_mask: torch.Tensor,
170+
index: np.ndarray,
171+
episode_index: np.ndarray,
172+
norm_adv_by_std_in_grpo: bool = True,
173+
epsilon: float = 1e-6,
174+
) -> tuple[torch.Tensor, torch.Tensor]:
175+
"""GRPO outcome advantage with the baseline computed at *episode* scope.
176+
177+
Mirrors ``verl.trainer.ppo.core_algos.compute_grpo_outcome_advantage`` but,
178+
instead of treating every sample equally when forming the per-task (``uid``)
179+
baseline, it first reduces every episode (``episode_uuids``) to its mean
180+
scalar reward and then computes the task baseline mean/std over those
181+
per-episode means. This way an episode that produced many samples does not
182+
dominate the baseline of an episode that produced few.
183+
184+
Example (matches the documented behaviour):
185+
task T -> episode 1 (2 samples, reward 1) + episode 2 (1 sample, reward 0)
186+
sample scope baseline = (1 + 1 + 0) / 3 = 0.667
187+
episode scope baseline = (mean[1, 1] + mean[0]) / 2 = (1 + 0) / 2 = 0.5
188+
189+
Args:
190+
token_level_rewards: (bsz, response_length) reward tensor.
191+
response_mask: (bsz, response_length) mask of trainable response tokens.
192+
index: per-sample task id (``non_tensor_batch["uid"]``).
193+
episode_index: per-sample episode id (``non_tensor_batch["episode_uuids"]``).
194+
norm_adv_by_std_in_grpo: divide the centred reward by the (episode-level)
195+
group std when True, otherwise only subtract the group mean.
196+
epsilon: numerical-stability term added to the std denominator.
197+
198+
Returns:
199+
(advantages, returns) - both (bsz, response_length); identical, as in GRPO.
200+
"""
201+
scores = (token_level_rewards * response_mask).sum(dim=-1) # (bsz,) scalar reward
202+
bsz = scores.shape[0]
203+
204+
# 1) reduce each episode to its mean scalar reward
205+
episode_score_sum: dict = defaultdict(float)
206+
episode_score_cnt: dict = defaultdict(int)
207+
for i in range(bsz):
208+
ep = episode_index[i]
209+
episode_score_sum[ep] += scores[i].item()
210+
episode_score_cnt[ep] += 1
211+
episode_mean = {ep: episode_score_sum[ep] / episode_score_cnt[ep] for ep in episode_score_sum}
212+
213+
# 2) collect, per task, the set of distinct episodes it produced
214+
task2episodes: dict = defaultdict(dict) # use dict as ordered set
215+
for i in range(bsz):
216+
task2episodes[index[i]][episode_index[i]] = None
217+
218+
# 3) per-task baseline = mean/std over the per-episode means.
219+
# Single-episode tasks are degenerate -> follow verl's convention
220+
# (mean=0, std=1) so the advantage reduces to the raw score.
221+
task_mean: dict = {}
222+
task_std: dict = {}
223+
for task, episodes in task2episodes.items():
224+
vals = torch.tensor([episode_mean[ep] for ep in episodes], dtype=torch.float32)
225+
if vals.numel() == 1:
226+
task_mean[task] = torch.tensor(0.0)
227+
task_std[task] = torch.tensor(1.0)
228+
else:
229+
task_mean[task] = vals.mean()
230+
task_std[task] = vals.std()
231+
232+
# 4) centre (and optionally normalise) every sample against its task baseline
233+
adv = scores.clone()
234+
for i in range(bsz):
235+
task = index[i]
236+
if norm_adv_by_std_in_grpo:
237+
adv[i] = (scores[i] - task_mean[task]) / (task_std[task] + epsilon)
238+
else:
239+
adv[i] = scores[i] - task_mean[task]
240+
241+
adv = adv.unsqueeze(-1) * response_mask
242+
return adv, adv
243+
244+
245+
def compute_episode_level_loss_weight(data: DataProto) -> torch.Tensor:
246+
"""Per-token loss weight that makes every episode contribute equally.
247+
248+
Each sample belonging to an episode (same ``non_tensor_batch["episode_uuids"]``)
249+
that produced ``N`` samples receives weight ``1 / N``. The weights of all
250+
samples of one episode therefore sum to 1, so an episode that emitted many
251+
samples does not contribute more to the loss than one that emitted few.
252+
253+
The weight is broadcast across the response dimension so it has the **same
254+
shape as ``advantages``** ((bsz, response_length)); this lets it multiply
255+
both the per-token policy-gradient term and the per-token KL term directly.
256+
257+
Returns:
258+
A (bsz, response_length) tensor (matching ``data.batch["advantages"]``
259+
dtype/device) of per-token loss weights, constant along the response
260+
dimension for a given sample.
261+
"""
262+
episode_index = data.non_tensor_batch["episode_uuids"]
263+
bsz = len(episode_index)
264+
episode_count: dict = defaultdict(int)
265+
for ep in episode_index:
266+
episode_count[ep] += 1
267+
advantages = data.batch["advantages"] # (bsz, response_length)
268+
per_sample = torch.tensor(
269+
[1.0 / episode_count[episode_index[i]] for i in range(bsz)],
270+
dtype=advantages.dtype,
271+
device=advantages.device,
272+
)
273+
# broadcast per-sample weight to the same shape as advantages
274+
weights = per_sample.view(-1, 1) * torch.ones_like(advantages)
275+
return weights
276+
277+
169278
def compute_advantage(
170279
data: DataProto,
171280
adv_estimator: AdvantageEstimator,
@@ -174,6 +283,7 @@ def compute_advantage(
174283
num_repeat: int = 1,
175284
norm_adv_by_std_in_grpo: bool = True,
176285
config: Optional[AlgoConfig] = None,
286+
advantage_estimation_episode_level: bool = False,
177287
) -> DataProto:
178288
"""Compute advantage estimates for policy optimization.
179289
@@ -189,13 +299,21 @@ def compute_advantage(
189299
norm_adv_by_std_in_grpo (bool, optional): Whether to normalize advantages by standard deviation in
190300
GRPO. Defaults to True.
191301
config (dict, optional): Configuration dictionary for algorithm settings. Defaults to None.
302+
advantage_estimation_episode_level (bool, optional): When True (and using the GRPO estimator),
303+
the GRPO baseline is computed at episode scope instead of sample scope so every episode
304+
contributes equally regardless of how many samples it produced. Defaults to False.
192305
193306
Returns:
194307
DataProto: The updated data with computed advantages and returns.
195308
"""
196309
# Back-compatible with trainers that do not compute response mask in fit
197310
if "response_mask" not in data.batch.keys():
198311
data.batch["response_mask"] = compute_response_mask(data)
312+
if advantage_estimation_episode_level and adv_estimator != AdvantageEstimator.GRPO:
313+
raise NotImplementedError(
314+
"ajet.trainer_common.advantage_estimation_episode_level is only "
315+
f"supported with the GRPO advantage estimator, got {adv_estimator}."
316+
)
199317
# prepare response group
200318
if adv_estimator == AdvantageEstimator.GAE:
201319
# Compute advantages and returns using Generalized Advantage Estimation (GAE)
@@ -222,13 +340,30 @@ def compute_advantage(
222340
response_length = grpo_calculation_mask.size(1)
223341
# This mask is the one intended for GRPO
224342
grpo_calculation_mask = data.batch["loss_mask"][:, -response_length:]
225-
# Call compute_grpo_outcome_advantage with parameters matching its definition
226-
advantages, returns = core_algos.compute_grpo_outcome_advantage(
227-
token_level_rewards=data.batch["token_level_rewards"],
228-
response_mask=grpo_calculation_mask,
229-
index=data.non_tensor_batch["uid"],
230-
norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo,
231-
)
343+
if advantage_estimation_episode_level:
344+
# Episode-scope baseline: every episode contributes equally to the
345+
# per-task baseline regardless of how many samples it produced.
346+
if "episode_uuids" not in data.non_tensor_batch:
347+
raise KeyError(
348+
"advantage_estimation_episode_level is enabled but "
349+
"non_tensor_batch['episode_uuids'] is missing; cannot identify "
350+
"same-episode samples."
351+
)
352+
advantages, returns = compute_grpo_episode_level_outcome_advantage(
353+
token_level_rewards=data.batch["token_level_rewards"],
354+
response_mask=grpo_calculation_mask,
355+
index=data.non_tensor_batch["uid"],
356+
episode_index=data.non_tensor_batch["episode_uuids"],
357+
norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo,
358+
)
359+
else:
360+
# Call compute_grpo_outcome_advantage with parameters matching its definition
361+
advantages, returns = core_algos.compute_grpo_outcome_advantage(
362+
token_level_rewards=data.batch["token_level_rewards"],
363+
response_mask=grpo_calculation_mask,
364+
index=data.non_tensor_batch["uid"],
365+
norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo,
366+
)
232367
data.batch["advantages"] = advantages
233368
data.batch["returns"] = returns
234369
else:
@@ -692,6 +827,13 @@ def fit(self): # noqa: C901
692827
"norm_adv_by_std_in_grpo", True
693828
) # GRPO adv normalization factor
694829

830+
# [AJET] episode-scope advantage baseline (disabled by default)
831+
advantage_estimation_episode_level = bool(
832+
self.config.ajet.trainer_common.get(
833+
"advantage_estimation_episode_level", False
834+
)
835+
)
836+
695837
batch = compute_advantage(
696838
batch,
697839
adv_estimator=self.config.algorithm.adv_estimator,
@@ -700,8 +842,26 @@ def fit(self): # noqa: C901
700842
num_repeat=self.config.ajet.rollout.num_repeat,
701843
norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo,
702844
config=self.config.algorithm,
845+
advantage_estimation_episode_level=advantage_estimation_episode_level,
703846
)
704847

848+
# [AJET] per-sample loss weight that makes every episode
849+
# contribute equally to the policy-gradient update
850+
# (disabled by default). Consumed in
851+
# AjetDataParallelPPOActor.update_policy.
852+
if bool(
853+
self.config.ajet.trainer_common.get(
854+
"loss_weight_normalization_episode_level", False
855+
)
856+
):
857+
if "episode_uuids" not in batch.non_tensor_batch:
858+
raise KeyError(
859+
"loss_weight_normalization_episode_level is enabled but "
860+
"non_tensor_batch['episode_uuids'] is missing; cannot "
861+
"identify same-episode samples."
862+
)
863+
batch.batch["loss_weight"] = compute_episode_level_loss_weight(batch)
864+
705865
# update critic
706866
if self.use_critic:
707867
with marked_timer("update_critic", timing_raw, color="pink"):

ajet/backbone/verl/dp_actor.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ def update_policy(self, data: DataProto):
161161
# Include rollout_log_probs for computing rollout_corr metrics in bypass mode
162162
if "rollout_log_probs" in data.batch.keys():
163163
select_keys.append("rollout_log_probs")
164+
# [AJET] per-sample loss weight (episode-level loss normalization).
165+
# Present only when ajet.trainer_common.loss_weight_normalization_episode_level
166+
# is enabled; absent => every sample weighted equally (default behaviour).
167+
if "loss_weight" in data.batch.keys():
168+
select_keys.append("loss_weight")
164169

165170
has_multi_modal_inputs = "multi_modal_inputs" in data.non_tensor_batch.keys()
166171
non_tensor_select_keys = []
@@ -208,6 +213,20 @@ def update_policy(self, data: DataProto):
208213
response_mask = model_inputs["response_mask"]
209214
old_log_prob = model_inputs["old_log_probs"]
210215
advantages = model_inputs["advantages"]
216+
# [AJET] Episode-level loss-weight normalization.
217+
# When ajet.trainer_common.loss_weight_normalization_episode_level
218+
# is enabled, every sample carries a per-token weight (1/N for
219+
# an episode that produced N samples), same shape as advantages.
220+
# Scaling the advantages by this positive weight scales each
221+
# sample's policy-gradient contribution by the same factor (the
222+
# clip/ratio behaviour is unchanged since the weight is a
223+
# positive per-sample constant); the same weight is applied to
224+
# the per-token KL term below, so every episode contributes
225+
# equally to the total loss.
226+
loss_weight = model_inputs.get("loss_weight", None)
227+
if loss_weight is not None:
228+
loss_weight = loss_weight.to(advantages.dtype)
229+
advantages = advantages * loss_weight
211230
# [AJET] Debug logging for tensor shapes
212231
input_ids = model_inputs["input_ids"]
213232
_shape_msg = f'[Update Policy] -> Micro batch shape, input_ids {input_ids.shape}, response {response_mask.shape} @{micro_batch_idx}/{num_micro_batches}'
@@ -294,6 +313,12 @@ def update_policy(self, data: DataProto):
294313
kld = kl_penalty(
295314
logprob=log_prob, ref_logprob=ref_log_prob, kl_penalty=self.config.kl_loss_type
296315
)
316+
# [AJET] apply the per-token episode-level loss weight to
317+
# the KL term as well (same weight/shape used for the
318+
# policy-gradient term above), so each episode contributes
319+
# equally to the KL loss too.
320+
if loss_weight is not None:
321+
kld = kld * loss_weight
297322
kl_loss = agg_loss(loss_mat=kld, loss_mask=response_mask, loss_agg_mode=loss_agg_mode)
298323

299324
policy_loss = policy_loss + kl_loss * self.config.kl_loss_coef

ajet/backbone/warm_up.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import asyncio
77
import logging
88
import os
9+
from datetime import datetime
910
from ajet.utils.async_utils import (
1011
apply_httpx_aclose_patch,
1112
silence_hermes_tool_parser_loggers,
@@ -14,21 +15,19 @@
1415
apply_httpx_aclose_patch()
1516
suppress_httpx_aclose_exception()
1617

17-
def init_parallel_rollout_logger(experiment_name, experiment_dir):
18+
def init_parallel_rollout_logger(experiment_dir):
1819
"""Initialize the logger with the given configuration."""
1920
if "PROCESS_LEVEL_WARMUP_INIT_LOGGER" in os.environ:
2021
return
21-
os.environ["PROCESS_LEVEL_WARMUP_INIT_LOGGER"] = "1"
2222

23-
from datetime import datetime
23+
os.environ["PROCESS_LEVEL_WARMUP_INIT_LOGGER"] = "1"
2424

2525
from beast_logger import register_logger
2626

2727
final_log_path = os.path.join(
2828
experiment_dir,
2929
datetime.now().strftime("%Y_%m_%d_%H_%M"),
30-
# machine host name
31-
os.uname().nodename,
30+
os.uname().nodename, # machine host name
3231
)
3332
os.environ["BEST_LOGGER_PATH"] = final_log_path
3433
non_console_mods = ["rollout", "token_clip", "bad_case"]
@@ -102,6 +101,6 @@ def warm_up_process(config):
102101
os.environ["PROCESS_LEVEL_WARMUP_INIT"] = "1"
103102
experiment_name = config.ajet.experiment_name
104103
experiment_dir = config.ajet.experiment_dir
105-
init_parallel_rollout_logger(experiment_name, experiment_dir)
104+
init_parallel_rollout_logger(experiment_dir)
106105
warm_up_task_judge_when_needed(config)
107106
clean_up_tmp_ajet_dir(config)

ajet/context_tracker/multiagent_tracking.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -440,13 +440,11 @@ def patch_prompt_tokens(
440440

441441
# HISTORY context length vs CURRENT prompt length
442442
if len(previous_ext_context) != len(prompt_text_split):
443-
logger.bind(exception=True).error(
444-
f"Length mismatch when patching prompt tokens. Previous ext context length: {len(previous_ext_context)}, prompt text split length: {len(prompt_text_split)}. Replacing all tokens."
445-
)
443+
logger.bind(exception=True).error(f"Length mismatch when patching prompt tokens. Previous ext context length: {len(previous_ext_context)}, prompt text split length: {len(prompt_text_split)}. Replacing all tokens.")
446444

447445
# try to recover tokens
448446
if self.config.ajet.context_tracker.fix_retokenization_drift:
449-
self.ensure_retokenization_perfect_match(
447+
previous_ext_context = self.ensure_retokenization_perfect_match(
450448
previous_ext_context, # HISTORY
451449
split_prompt_token_ids, # CURRENT
452450
prompt_text_split, # CURRENT
@@ -481,18 +479,17 @@ def ensure_retokenization_perfect_match(self, previous_ext_context, split_prompt
481479
# otherwise, we throw a warning (do not worry, this causes almost no influence in the training)
482480
print_dict(
483481
{
484-
"expected_prompt_text": prompt_text_split[j], # from llm_output["prompt_text"]
485-
"current_prompt_text": current_prompt_text[j], # history prompt text converted from token_arr to text using tokenizer
486-
"expected_token_ids": vllm_token_array, # from llm_output["prompt_token_ids"]
487-
"current_token_ids": tracker_token_array, # from previous_ext_context[j].token_arr
482+
"expected_prompt_text": prompt_text_split[j], # from llm_output["prompt_text"], converted directly from messages using apply_chat_template, passway (messages->apply_chat_template->text)
483+
"current_prompt_text": current_prompt_text[j], # history prompt text converted from token_arr to text using tokenizer, passway (messages->extended_message->incremental apply_chat_template->token_arr->text)
484+
"expected_token_ids": vllm_token_array, # from llm_output["prompt_token_ids"], passway (messages->apply_chat_template->token)
485+
"current_token_ids": tracker_token_array, # from previous_ext_context[j].token_arr, passway (messages->extended_message->incremental apply_chat_template->token_arr)
488486
},
489487
mod="exception",
490-
header="Prompt token ids mismatch.",
488+
header="Prompt token ids mismatch (fixing drift by `token_arr=vllm_token_array`).",
491489
)
492-
# # fix drift
493-
# previous_ext_context[j].token_arr = self.tokenizer(
494-
# prompt_text_split[j], return_tensors="pt", padding=False
495-
# )["input_ids"].tolist()
490+
previous_ext_context[j].token_arr = vllm_token_array
491+
return previous_ext_context
492+
496493

497494
def process_reward(self, reward_structure: Reward):
498495
self.reward_structure = reward_structure

0 commit comments

Comments
 (0)