Skip to content

Commit 4c0b65f

Browse files
committed
add cache to tokenizer
1 parent 2468d05 commit 4c0b65f

12 files changed

Lines changed: 497 additions & 40 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,4 @@ cocktail_vs_separate*
190190
cocktail_results_*
191191
PRINCIPLE.md
192192
TODO.md
193+
plot_mean_reward.py

ajet/backbone/main_verl.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,16 @@ def run(self, config):
132132

133133
# Instantiate the tokenizer and processor.
134134
from verl.utils import hf_processor, hf_tokenizer
135+
from ajet.tokenizer.service import start_tokenizer_service
135136

136137
trust_remote_code = config.data.get("trust_remote_code", False)
137-
tokenizer = hf_tokenizer(local_path, trust_remote_code=trust_remote_code)
138+
local_tokenizer = hf_tokenizer(local_path, trust_remote_code=trust_remote_code)
139+
# Cache hot tokenization calls (encode / decode / apply_chat_template)
140+
# in a sidecar process; every other tokenizer attribute is served by
141+
# the local instance directly.
142+
tokenizer = start_tokenizer_service(
143+
local_tokenizer, local_path, trust_remote_code=trust_remote_code
144+
)
138145
# Used for multimodal LLM, could be None
139146
processor = hf_processor(local_path, trust_remote_code=trust_remote_code, use_fast=True)
140147

ajet/backbone/trainer_verl.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,15 +511,16 @@ def fit(self): # noqa: C901
511511
]
512512
)
513513
)
514-
logger.info("start fit rollout")
514+
logger.info("start batch rollout")
515515
self.parallel_env.current_global_steps = self.global_steps
516+
# rollout stage begin ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
516517
context_tracker_arr: List[SingleAgentContextTracker] = self.parallel_env.rollout(
517518
tasks, mode="sample", epoch=f"train.{epoch}"
518519
)
519520

520521
# from ajet import bp; bp("BATCH")
521522

522-
logger.info("end fit rollout")
523+
logger.info("end batch rollout")
523524
gen_batch_output = self.parallel_env.to_dataproto(context_tracker_arr)
524525
logger.info("end dataproto convertion")
525526

@@ -710,7 +711,7 @@ def fit(self): # noqa: C901
710711

711712
# implement critic warmup
712713
if self.config.trainer.critic_warmup <= self.global_steps:
713-
# update actor
714+
# update actor ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
714715
with marked_timer("update_actor", timing_raw, color="red"):
715716
actor_output = self._update_actor(batch)
716717
actor_output_metrics = reduce_metrics(actor_output.meta_info["metrics"])

ajet/context_tracker/multiagent_tracking.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def step_spawn_timeline(self, messages: List[dict], tools: List = [], disable_to
130130
if disable_toolcalls:
131131
consider_roles.remove("tool")
132132

133+
previous_message_encounter_user_role = False
134+
133135
for i, msg in enumerate(messages):
134136

135137
if (disable_toolcalls) and (not isinstance(msg["content"], str)):
@@ -166,6 +168,11 @@ def step_spawn_timeline(self, messages: List[dict], tools: List = [], disable_to
166168
else:
167169
author = "env"
168170

171+
if msg["role"] == "user":
172+
previous_message_encounter_user_role = True
173+
174+
any_later_msg_has_user_role = any((m["role"] == "user") for m in messages[i+1:])
175+
169176
# extract content block from openai-competible messages and convert to ExtendedMessage
170177
timeline += [
171178
ExtendedMessage(
@@ -179,8 +186,11 @@ def step_spawn_timeline(self, messages: List[dict], tools: List = [], disable_to
179186
token_generator="auto",
180187
name = (msg["name"] if "name" in msg else ""),
181188
first_message=(i == 0),
189+
before_last_query=any_later_msg_has_user_role
182190
)
183191
]
192+
if ("<think>" in msg["content"]) and (not previous_message_encounter_user_role):
193+
logger.warning(f"Warning! Message content contains <think> tag, but no prior message has `user` role! This is not a common scenario. Please check your agent loop carefully.")
184194

185195
return timeline
186196

@@ -393,9 +403,9 @@ def patch_prompt_tokens(
393403
) -> List[ExtendedMessage]:
394404
"""
395405
fix retokenization drift
396-
prompt_text: [this llm call] the prompt in text format used in generation
397-
prompt_token_ids: [this llm call] the prompt token ids used in generation (prompt_text->prompt_token_ids using tokenizer)
398-
previous_ext_context: [from previous context] the context history
406+
prompt_text = llm_output["prompt_text"]: [this llm call] the prompt in text format used in generation
407+
prompt_token_ids = llm_output["prompt_token_ids"]: [this llm call] the prompt token ids used in generation (prompt_text->prompt_token_ids using tokenizer)
408+
previous_ext_context: [from previous context] the context history
399409
"""
400410

401411
# remove tailing, usually `<|im_start|> assistant`
@@ -467,21 +477,22 @@ def ensure_retokenization_perfect_match(self, previous_ext_context, split_prompt
467477
# good, everything is perfect
468478
continue
469479
else:
480+
from ajet import bp; bp("SWARM")
470481
# otherwise, we throw a warning (do not worry, this causes almost no influence in the training)
471482
print_dict(
472483
{
473-
"expected_prompt_text": prompt_text_split[j],
474-
"current_prompt_text": current_prompt_text[j],
475-
"expected_token_ids": split_prompt_token_ids[j],
476-
"current_token_ids": previous_ext_context[j].token_arr,
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
477488
},
478489
mod="exception",
479490
header="Prompt token ids mismatch.",
480491
)
481-
# fix drift
482-
previous_ext_context[j].token_arr = self.tokenizer(
483-
prompt_text_split[j], return_tensors="pt", padding=False
484-
)["input_ids"]
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()
485496

486497
def process_reward(self, reward_structure: Reward):
487498
self.reward_structure = reward_structure

ajet/context_tracker/single_agent_tracking.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def get_token_inc_from_llm_response(
9191
self.generated_token_cnt += len(vllm_output_raw_token)
9292
if not self.generation_prompt_token:
9393
self.generation_prompt_token = self.get_generation_prompt_token()
94-
final_token_arr, token_logprob_arr, loss_mask, lack_normal_eos = replace_token_ids(
94+
final_token_arr, token_logprob_arr, loss_mask, lack_normal_eos = replace_token_ids( # pad tokens and logprobs with begin_ids / other_ids / NA
9595
token_container=completion_token_arr,
9696
precise_token=vllm_output_raw_token,
9797
precise_logprob=vllm_output_raw_logprob,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
3+
Your task is to investigate the chat template of given model, go to its tokenizer config and check whether the following behavior exists:
4+
5+
>
6+
> Remove history <think> block from the input when apply chat template when converting messages.
7+
>
8+
9+
This behavior will make RL training slower, if this behavior exists, please change the chat template to forbid such behavior.
10+
11+
You must not do this in-place, instead, please create another model.
12+
E.g., "/mnt/data_cpfs/xielipeng.xlp/models/Qwen3-8B" -> "/mnt/data_cpfs/xielipeng.xlp/models/Qwen3-8B-Keep-History"
13+
For all files within the original model path, please create symbolic links instead of copying files.
14+
With only one exception, the tokenizer config file, which should be copied and modified to change the chat template.
15+
16+

ajet/schema/extended_msg.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"memory",
2121
"llm(do_not_train)",
2222
]
23-
DUMMY_MSG = [{"role": "user", "content": "dummy text"}]
23+
2424

2525

2626
def find_sublist_indices(large_list, small_list, reverse=False):
@@ -77,6 +77,7 @@ def __init__(
7777
token_logprob_arr=[],
7878
name="", # preserved field, not used currently
7979
first_message=False,
80+
before_last_query=True, # whether this message is before the last user query in the conversation, used for auto tokenization logic
8081
):
8182
self.author = author
8283
self.role = role
@@ -107,6 +108,7 @@ def __init__(
107108
self.eos_token_id = tokenizer.eos_token_id
108109

109110
if token_generator == "auto":
111+
self.before_last_query = before_last_query
110112
self.token_arr = self.auto_tokenize(
111113
tokenizer=tokenizer,
112114
tools=tools,
@@ -137,6 +139,12 @@ def auto_tokenize(self, tokenizer, tools):
137139
return self.token_arr
138140

139141
def auto_tokenize_non_first_message(self, tokenizer, tools):
142+
if self.before_last_query:
143+
# for example, this will remove the <thinking> block for qwen3's chat template
144+
dummy_msg = [{"role": "assistant", "content": "dummy text"}]
145+
else:
146+
dummy_msg = [{"role": "user", "content": "dummy text"}]
147+
140148
try:
141149
# completion_token_arr will contain generation_prompt header
142150
auto_tokenize_target:dict = {
@@ -149,7 +157,7 @@ def auto_tokenize_non_first_message(self, tokenizer, tools):
149157
auto_tokenize_target.update({"tool_call_id": self.tool_call_id})
150158
text_frag_to = ajet_apply_chat_template(
151159
tokenizer=tokenizer,
152-
conversation=DUMMY_MSG + [auto_tokenize_target],
160+
conversation=dummy_msg + [auto_tokenize_target],
153161
tokenize=False,
154162
tools=tools,
155163
)
@@ -160,7 +168,7 @@ def auto_tokenize_non_first_message(self, tokenizer, tools):
160168
self.token_arr, _ = self.get_inc_simple(
161169
text_frag_from=ajet_apply_chat_template(
162170
tokenizer=tokenizer,
163-
conversation=DUMMY_MSG,
171+
conversation=dummy_msg,
164172
tokenize=False,
165173
tools=tools,
166174
),
@@ -314,21 +322,23 @@ def merge_tool_group(group, tokenizer):
314322
token_logprob_arr=msg0.token_logprob_arr,
315323
first_message=msg0.first_message,
316324
)
325+
# a dummy msg, not necessary, can be []
326+
dummy_msg = [{"role": "user", "content": "dummy text"}]
317327
# re-compute token_arr
318328
auto_tokenize_targets = [
319329
{"role": msg.role, "content": msg.text_content_for_compare} for msg in group
320330
]
321331
merged.token_arr, _ = merged.get_inc_simple(
322332
text_frag_from=ajet_apply_chat_template(
323333
tokenizer=tokenizer,
324-
conversation=DUMMY_MSG,
334+
conversation=dummy_msg,
325335
tokenize=False,
326336
tools=merged.tools,
327337
add_generation_prompt=False,
328338
),
329339
text_frag_to=ajet_apply_chat_template(
330340
tokenizer,
331-
conversation=DUMMY_MSG + auto_tokenize_targets,
341+
conversation=dummy_msg + auto_tokenize_targets,
332342
tokenize=False,
333343
tools=merged.tools,
334344
add_generation_prompt=False,

ajet/tokenizer/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Intentionally empty. Import directly from ``ajet.tokenizer.service``:
2+
#
3+
# from ajet.tokenizer.service import RemoteTokenizer, start_tokenizer_service
4+
#
5+
# Re-exporting here would cause ``python -m ajet.tokenizer.service`` to load
6+
# the package eagerly before running the service as __main__, which trips a
7+
# RuntimeWarning from runpy.

0 commit comments

Comments
 (0)