Spaces:
Sleeping
Sleeping
Force Qwen3 chat template to disable thinking mode via tokenizer patch
Browse filesDiagnosed the 100% clipped_ratio and entropy=0.07 collapse: Qwen3's chat
template was auto-inserting <think>...</think> prefixes, and the 0.6B
model was burning its entire 512-token completion budget on reasoning
text without ever emitting a tool call. The GRPOConfig flag
chat_template_kwargs={'enable_thinking': False} was silently dropped by
the installed TRL version.
Fix: load tokenizer with AutoTokenizer, wrap its apply_chat_template so
every invocation passes enable_thinking=False, then hand the patched
tokenizer to GRPOTrainer via processing_class (falling back to the older
tokenizer kwarg when needed). A startup probe logs whether <think>
tokens still appear in the generation prompt.
Made-with: Cursor
- training/train_grpo.py +47 -1
training/train_grpo.py
CHANGED
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
| 12 |
from typing import Literal
|
| 13 |
|
| 14 |
from datasets import load_dataset
|
| 15 |
-
from transformers import TrainerCallback
|
| 16 |
from trl import GRPOConfig, GRPOTrainer
|
| 17 |
|
| 18 |
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -434,6 +434,43 @@ class LiveLogHistoryCallback(TrainerCallback):
|
|
| 434 |
self._write(state.log_history)
|
| 435 |
|
| 436 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
def summarize_log_history(log_history):
|
| 438 |
reward_rows = [row for row in log_history if "reward" in row]
|
| 439 |
if not reward_rows:
|
|
@@ -481,11 +518,14 @@ def main():
|
|
| 481 |
dataset = load_dataset("json", data_files={"train": args.train_file})
|
| 482 |
training_args = build_config(args)
|
| 483 |
|
|
|
|
|
|
|
| 484 |
print("Initializing GRPO Trainer...", flush=True)
|
| 485 |
trainer_kwargs = {
|
| 486 |
"model": args.model_name,
|
| 487 |
"args": training_args,
|
| 488 |
"train_dataset": dataset["train"],
|
|
|
|
| 489 |
}
|
| 490 |
|
| 491 |
if supports_env_factory:
|
|
@@ -499,6 +539,12 @@ def main():
|
|
| 499 |
)
|
| 500 |
trainer_kwargs["reward_funcs"] = [compatibility_reward_func]
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
trainer = GRPOTrainer(**trainer_kwargs)
|
| 503 |
trainer.add_callback(LiveLogHistoryCallback(args.metrics_json))
|
| 504 |
|
|
|
|
| 12 |
from typing import Literal
|
| 13 |
|
| 14 |
from datasets import load_dataset
|
| 15 |
+
from transformers import AutoTokenizer, TrainerCallback
|
| 16 |
from trl import GRPOConfig, GRPOTrainer
|
| 17 |
|
| 18 |
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 434 |
self._write(state.log_history)
|
| 435 |
|
| 436 |
|
| 437 |
+
def _build_tokenizer_without_thinking(model_name: str):
|
| 438 |
+
"""
|
| 439 |
+
Load the tokenizer and force Qwen3's chat template to skip the <think> block.
|
| 440 |
+
|
| 441 |
+
Qwen3 chat templates default to enable_thinking=True, which prefixes every
|
| 442 |
+
assistant turn with '<think>\\n</think>\\n\\n' and causes the model to burn
|
| 443 |
+
the entire completion budget on reasoning text without emitting a tool call.
|
| 444 |
+
TRL's GRPOConfig on older versions silently drops chat_template_kwargs, so
|
| 445 |
+
we enforce the flag by wrapping apply_chat_template at the tokenizer level.
|
| 446 |
+
"""
|
| 447 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 448 |
+
original_apply = tokenizer.apply_chat_template
|
| 449 |
+
|
| 450 |
+
def apply_no_thinking(*args, **kwargs):
|
| 451 |
+
kwargs.setdefault("enable_thinking", False)
|
| 452 |
+
return original_apply(*args, **kwargs)
|
| 453 |
+
|
| 454 |
+
try:
|
| 455 |
+
tokenizer.apply_chat_template = apply_no_thinking
|
| 456 |
+
probe = tokenizer.apply_chat_template(
|
| 457 |
+
[{"role": "user", "content": "ping"}],
|
| 458 |
+
tokenize=False,
|
| 459 |
+
add_generation_prompt=True,
|
| 460 |
+
)
|
| 461 |
+
if "<think>" in probe:
|
| 462 |
+
print(
|
| 463 |
+
"Warning: chat template still emits <think> tokens; thinking mode may be on.",
|
| 464 |
+
flush=True,
|
| 465 |
+
)
|
| 466 |
+
else:
|
| 467 |
+
print("Chat template confirmed with enable_thinking=False.", flush=True)
|
| 468 |
+
except Exception as exc:
|
| 469 |
+
print(f"Chat template probe failed: {exc}", flush=True)
|
| 470 |
+
|
| 471 |
+
return tokenizer
|
| 472 |
+
|
| 473 |
+
|
| 474 |
def summarize_log_history(log_history):
|
| 475 |
reward_rows = [row for row in log_history if "reward" in row]
|
| 476 |
if not reward_rows:
|
|
|
|
| 518 |
dataset = load_dataset("json", data_files={"train": args.train_file})
|
| 519 |
training_args = build_config(args)
|
| 520 |
|
| 521 |
+
tokenizer = _build_tokenizer_without_thinking(args.model_name)
|
| 522 |
+
|
| 523 |
print("Initializing GRPO Trainer...", flush=True)
|
| 524 |
trainer_kwargs = {
|
| 525 |
"model": args.model_name,
|
| 526 |
"args": training_args,
|
| 527 |
"train_dataset": dataset["train"],
|
| 528 |
+
"processing_class": tokenizer,
|
| 529 |
}
|
| 530 |
|
| 531 |
if supports_env_factory:
|
|
|
|
| 539 |
)
|
| 540 |
trainer_kwargs["reward_funcs"] = [compatibility_reward_func]
|
| 541 |
|
| 542 |
+
trainer_params = inspect.signature(GRPOTrainer.__init__).parameters
|
| 543 |
+
if "processing_class" not in trainer_params:
|
| 544 |
+
trainer_kwargs.pop("processing_class", None)
|
| 545 |
+
if "tokenizer" in trainer_params:
|
| 546 |
+
trainer_kwargs["tokenizer"] = tokenizer
|
| 547 |
+
|
| 548 |
trainer = GRPOTrainer(**trainer_kwargs)
|
| 549 |
trainer.add_callback(LiveLogHistoryCallback(args.metrics_json))
|
| 550 |
|