Repository navigation
Conversation
|
thks, can you please provide a minimal repro? |
1 similar comment
|
thks, can you please provide a minimal repro? |
|
Sure. Only the HTTP call is stubbed, everything else is real TRL code. import asyncio
import torch
from trl.experimental.async_grpo.async_grpo_trainer import DataCollatorForRollout
from trl.experimental.async_grpo.async_rollout_worker import _AsyncRolloutLoop
# vLLM serializes a NaN logprob as JSON null, so the client receives None
# for a token the engine could not score.
VLLM_RESPONSE = {
"choices": [{"token_ids": [10, 11, 12], "logprobs": {"token_logprobs": [-0.5, None, -0.7]}}]
}
loop = _AsyncRolloutLoop.__new__(_AsyncRolloutLoop)
loop.model_name, loop.max_tokens, loop.temperature = "m", 8, 1.0
loop.top_p, loop.top_k, loop.repetition_penalty = 1.0, -1, 1.0
loop.min_p, loop.request_timeout = None, 1
loop._post = lambda *a, **k: asyncio.sleep(0, result=VLLM_RESPONSE)
token_ids, logprobs = asyncio.run(loop._generate_one_turn([1, 2]))
print(f"1. worker returns: {logprobs}")
sample = {
"input_ids": [1, 2] + token_ids,
"completion_mask": [0, 0, 1, 1, 1],
"old_log_probs": [0.0, 0.0] + logprobs,
"advantage": 1.0,
"group_id": 0,
"metrics": {"reward": 1.0},
}
try:
DataCollatorForRollout(pad_token_id=0, num_processes=1).torch_call([[[sample]]])
print("2. collator: ok")
except TypeError as e:
print(f"2. collator crashes: {type(e).__name__}: {e}")
# Why the loss also needs handling: had the value arrived as NaN rather than
# None there is no crash, it propagates through the ratio instead.
old = torch.tensor([[-0.5, float("nan"), -0.7]])
new = torch.tensor([[-0.4, -0.6, -0.8]], requires_grad=True)
loss = -(torch.exp(new - old) * torch.ones_like(old)).mean()
print(f"3. loss with one NaN old logprob: {loss.item()}")On On this branch: Step 3 prints An end to end repro against a live vLLM server needs a token the engine actually |
|
No but a real training repro |
I guess you can recover this from the logs |
|
Fair point on the logs. I don't have a long async production run to mine, but that For the training repro, I rented an H100 NVL and ran the real trainer end to end import os
import torch
from datasets import load_dataset
from transformers import AutoTokenizer
from tests.experimental.test_async_grpo_trainer import (
_StubRolloutWorker,
_StubWeightTransfer,
dummy_reward_func,
)
from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer
# INJECT=none -> what main's worker emits for an unscorable token (JSON null -> None)
# INJECT=nan -> what the post-fix worker emits (null mapped to NaN at ingestion)
BAD = None if os.environ.get("INJECT", "none") == "none" else float("nan")
class OneUnscorableTokenWorker(_StubRolloutWorker):
def _fill_queue(self):
super()._fill_queue()
sample = self.rollout_buffer.get()
sample.old_log_probs[-1] = BAD
self.rollout_buffer.put(sample)
model_id = "trl-internal-testing/small-Qwen2ForCausalLM-2.5"
dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_completion", split="train")
args = AsyncGRPOConfig(
output_dir="async-grpo-unscorable-repro",
max_steps=2,
per_device_train_batch_size=3,
num_generations=3,
max_completion_length=8,
token_budget=256,
vllm_server_timeout=5.0,
report_to="none",
)
trainer = AsyncGRPOTrainer(
model=model_id,
reward_funcs=dummy_reward_func,
args=args,
train_dataset=dataset,
rollout_worker=OneUnscorableTokenWorker(AutoTokenizer.from_pretrained(model_id), dataset, num_generations=3),
weight_transfer=_StubWeightTransfer(),
)
trainer.train()
losses = [e["loss"] for e in trainer.state.log_history if "loss" in e]
nan_params = any(torch.isnan(p).any().item() for p in trainer.model.parameters())
print("per-step losses:", losses)
print("final train_loss:", trainer.state.log_history[-1].get("train_loss"))
print("model has NaN parameters:", nan_params)Observed results: main (6d484ba), INJECT=none: main, INJECT=nan (this is the part I find most convincing): The reported loss curve looks completely healthy while the weights are already this branch, INJECT=nan: On the same H100 I also ran One caveat for completeness: INJECT=none still crashes on this branch, by Happy to add the NaN variant as a regression test next to |
|
so tldr you never encountered this issue in one of your training? |
|
No. Found it reading #6693, which fixed this for GRPOTrainer and |
|
I wrote #6693 and the #6862 metric this PR cites, so this is familiar ground. Instead of just reading the diff I checked out the branch and ran your tests both ways. Everything checks out:
One thought, not a blocker. In the sync trainer this situation is handled by dropping the token from the importance-sampling correction, while your fix trains it as plain on-policy policy gradient with ratio 1. For sync GRPO those are basically the same thing. For AsyncGRPO they are not quite: the rollout can be several policy versions old, so forcing the ratio to 1 on a genuinely off-policy token introduces a small bias. It is bounded, it only hits tokens vLLM itself could not score, and it clearly beats a crash or a NaN loss, so I would keep your approach. I would just add a one-line comment saying the token is treated as on-policy even when the rollout is stale, so the next person changing this code knows it was a deliberate choice. #6945 is related if you want the bigger picture on AsyncGRPO and importance sampling. Nice work closing this out for the async trainer, and thanks for the real regression coverage. |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
Is this a real or theoretical failure mode? |
|
Real for the sync trainer: #6166 is a field report, an 8xA100 run where entropy collapse made vLLM emit a NaN logprob and training died. For async I have only reproduced it with a stubbed vLLM response and in the H100 run above. I haven't seen it in a long async production run. The async path has the same trigger and gets Also added the one-line comment @behroozazarkhalili suggested, noting the token is deliberately treated as on-policy even when the rollout is stale. |
|
Correction to my last comment, after checking the vLLM server path more carefully. The worker calls So for a NaN logprob the async trainer never receives I have rewritten the PR description and the two code comments to say this (f134993). If you would rather not carry a defensive guard for a condition nobody has reported on the async path, I am fine closing this. |
|
So IIUC: first it was about supporting NaN, that comes from a run that had already collapsed, then a NaN that never reaches the trainer, now a null that, as far as I can tell, only shows up with echo=True, which we don't use? Closing because it seems that this PR never originated from a bug in genuine training. Please re-open with a clear motivation if I missed something |
What does this PR do?
AsyncGRPOTrainergets per-token generator logprobs from the vLLM server overHTTP (
/v1/completions). vLLM's completion response typestoken_logprobsaslist[float | None]and emits JSONnullfor a step it has no logprob for, sothe rollout worker can receive
Noneintoken_logprobs. Two consequences:torch.tensor([...], dtype=torch.float32)over those values, and a
NoneraisesTypeError: must be real number, not NoneType, ending a long training run on a single token.old_log_probsmakes the PPOratio and the whole loss NaN, and the optimizer still steps on it. The
Trainernan/inf logging filter substitutes the running mean (GRPO: a NaN training step is invisible in log_history, so a corrupted run reports a clean loss curve #6702), sothe loss curve stays plausible. Make a non-finite training step visible in log_history #6862 makes that condition visible by
reporting
frac_nonfinite_loss; this PR removes one of the causes it wouldreport, so the two are complementary.
Scope note, corrected after review: the NaN logprob from #6166 does not
reach the async trainer as
null. The worker's request is non-streaming, sovLLM serializes the response with starlette's
json.dumps(allow_nan=False),which raises on NaN; vLLM's
ValueErrorhandler turns that into an HTTP errorand the worker retries. The
nullthis PR guards against is the missing-stepcase, and the NaN guard in the loss is defensive. I have not observed either in
a production async run; the behaviour is demonstrated by the tests and the
injected-sample run in the thread.
#6693 hardened the sync
GRPOTrainerandgrpo_with_replay_bufferagainstNonelogprobs. The async trainer was left behind. This PR completes it:async_rollout_worker.py: mapNonetofloat("nan")when parsing thecompletions response. Both rollout paths share this method. The existing
0.0sentinel is deliberately not reused, since0.0is a legitimatelogprob and these are trained (mask=1) positions.
async_grpo_trainer.py: incompute_loss, replace NaNold_log_probswithlog_probs.detach()before the ratio.log_ratiobecomes exactly 0, so thecoefficient is exactly 1 and clipping is inert, while the gradient still
flows through
log_probs. The token trains as plain policy gradient and theKL and clip metrics stay finite. This is the same fallback the sync trainer
already uses when old logprobs are unavailable. The token is treated as
on-policy even when the rollout is stale, a small deliberate bias.
Three regression tests in
TestUnscorableTokenLogprobspin the ingestionmapping, the collator handling NaN rows, and the loss neutralization (finite
loss, finite gradients, finite KL). The ingestion and loss tests each fail
without their half of the fix.
#6297 covers the same class for the sync trainer only and predates #6693.
#6862 is complementary rather than overlapping: it surfaces a non-finite loss
after the fact in the same trainers and cites #6166 as one of the causes. This
PR prevents that cause, and additionally fixes the crash, which no other open
PR addresses.
Before submitting
AI writing disclosure
We welcome the use of AI tools to help with contributions. For transparency and to help us improve our review process, please indicate the level of AI involvement in this PR.
Who can review?
cc @qgallouedec
Note
Medium Risk
Changes RL training loss semantics for unscorable tokens (on-policy fallback) and touches the async rollout path; scope is narrow with targeted tests but affects gradient updates when vLLM omits logprobs.
Overview
Async GRPO now survives vLLM returning JSON
nullfor unscorable per-token logprobs (serialized NaN), which previously could crash collation (Nonein tensors) or poison the PPO loss with NaN weights while logs looked fine.The rollout worker maps
None→float("nan")when parsingtoken_logprobs. Incompute_loss, NaNold_log_probsare replaced with the current policy logprob (detached), matching sync GRPO: importance ratio 1, finite KL/clip metrics, gradients still flow.TestUnscorableTokenLogprobsadds three regression tests for ingestion, collator tensorization, and loss/gradient finiteness.Reviewed by Cursor Bugbot for commit f134993. Bugbot is set up for automated code reviews on this repo. Configure here.