fix(grpo): tail-truncate context inside _to_prompt to satisfy vllm input-length gate
Browse files- train/grpo.py +27 -1
train/grpo.py
CHANGED
|
@@ -184,13 +184,39 @@ def run_grpo(
|
|
| 184 |
"respond with the shortest exact answer span."
|
| 185 |
)
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
def _to_prompt(example: dict) -> dict:
|
|
|
|
|
|
|
| 188 |
msgs = [
|
| 189 |
{"role": "system", "content": sys_msg},
|
| 190 |
{
|
| 191 |
"role": "user",
|
| 192 |
"content": (
|
| 193 |
-
f"Context:\n{
|
| 194 |
f"{example.get('prompt','')}"
|
| 195 |
),
|
| 196 |
},
|
|
|
|
| 184 |
"respond with the shortest exact answer span."
|
| 185 |
)
|
| 186 |
|
| 187 |
+
# vLLM hard-checks final prompt length against the model's max_position_embeddings
|
| 188 |
+
# (32 768 for Qwen 2.5 Coder 0.5B/1.5B). Our train.jsonl `context` field is the
|
| 189 |
+
# full long document (often >40K tokens). `max_prompt_length` in GRPOConfig is a
|
| 190 |
+
# post-tokenization cap that TRL applies AFTER vLLM has already rejected the
|
| 191 |
+
# request (`vllm.exceptions.VLLMValidationError: prompt contains 37220 tokens`).
|
| 192 |
+
# We must truncate inside `_to_prompt`, BEFORE chat templating.
|
| 193 |
+
#
|
| 194 |
+
# Budget: keep the prompt comfortably under cfg.train.max_prompt_length so the
|
| 195 |
+
# system message + chat-template overhead don't push us over. Tail-truncate the
|
| 196 |
+
# context (recent text usually contains the answer span in our synthetic data).
|
| 197 |
+
max_prompt_tok = int(cfg.train.max_prompt_length)
|
| 198 |
+
chat_overhead_tok = 256 # system msg + chat template wrappers + question
|
| 199 |
+
ctx_budget_tok = max(256, max_prompt_tok - chat_overhead_tok)
|
| 200 |
+
|
| 201 |
+
def _truncate_to_tokens(text: str, max_tokens: int) -> str:
|
| 202 |
+
if not text:
|
| 203 |
+
return ""
|
| 204 |
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
| 205 |
+
if len(ids) <= max_tokens:
|
| 206 |
+
return text
|
| 207 |
+
# Keep the tail: synthetic gold answers are sampled across the doc, so tail
|
| 208 |
+
# is no worse than head and is cheaper to slice.
|
| 209 |
+
return tokenizer.decode(ids[-max_tokens:], skip_special_tokens=True)
|
| 210 |
+
|
| 211 |
def _to_prompt(example: dict) -> dict:
|
| 212 |
+
ctx_full = example.get("context", "") or ""
|
| 213 |
+
ctx_truncated = _truncate_to_tokens(ctx_full, ctx_budget_tok)
|
| 214 |
msgs = [
|
| 215 |
{"role": "system", "content": sys_msg},
|
| 216 |
{
|
| 217 |
"role": "user",
|
| 218 |
"content": (
|
| 219 |
+
f"Context:\n{ctx_truncated}\n\n"
|
| 220 |
f"{example.get('prompt','')}"
|
| 221 |
),
|
| 222 |
},
|