fix(train): reject a train_batch_size that truncates the prompt batch to zero - #1985
fix(train): reject a train_batch_size that truncates the prompt batch to zero#1985pranavraja99 wants to merge 1 commit into
Conversation
… to zero `_remove_tail_data` keeps the largest multiple of `stride = lcm_dp_size // gcd(lcm_dp_size, n_samples_per_prompt)` prompts. When `train_batch_size` is below that stride the batch truncates to zero prompts, the generator is called with no prompts, and the run dies in `get_rollout_metrics` on `np.min` of an empty array. `validate_batch_sizes` only checked `train_batch_size * n_samples_per_prompt >= lcm_dp_size`, which is necessary but not sufficient: with `lcm_dp_size=6` and `n_samples_per_prompt=4`, a `train_batch_size` of 2 passes (8 >= 6) while the stride is 3. Check the stride directly at startup instead, and warn when `train_batch_size` is above the stride but not a multiple of it, since that silently drops prompts from every training batch. Also raise in `_remove_tail_data` itself, which covers the callers whose batch size is not `train_batch_size` (the async trainer and skyrl-agent). Closes NovaSky-AI#1609 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request improves batch size validation and handling in SkyRL. It introduces a check to ensure that the prompt batch size is not smaller than the prompt stride, preventing all prompts from being dropped during truncation. It updates validate_batch_sizes to assert this constraint and warn when the batch size is not a multiple of the stride, and adds corresponding unit tests. The reviewer suggested refining the check in _remove_tail_data to ensure that a ValueError is not misleadingly raised when the input batch is already empty.
| return entries | ||
|
|
||
| kept_prompts = (len(entries) // stride) * stride | ||
| if kept_prompts == 0: |
There was a problem hiding this comment.
If entries is empty (i.e., len(entries) == 0), kept_prompts will be 0. In this case, raising a ValueError stating that the batch is smaller than the prompt stride is misleading because the batch was already empty, not truncated to zero. Adding a check for len(entries) > 0 ensures we only raise this error when actual prompts are being truncated to zero.
| if kept_prompts == 0: | |
| if len(entries) > 0 and kept_prompts == 0: |
Closes #1609.
RayPPOTrainer._remove_tail_datakeeps the largest multiple ofstride = lcm_dp_size // gcd(lcm_dp_size, n_samples_per_prompt)prompts so that the resulting samples shard evenly across the data-parallel sizes of the enabled models. Whentrain_batch_sizeis below that stride,(len(entries) // stride) * strideis0: the generator is called with zero prompts and the run dies several frames later inget_rollout_metricsonnp.minof an empty array (ValueError: zero-size array to reduction operation minimum which has no identity), with nothing pointing at the batch size.validate_batch_sizesdid check a related condition,train_batch_size * n_samples_per_prompt >= lcm_dp_size, but that is necessary rather than sufficient. For the config in the issue (lcm_dp_size=6,n_samples_per_prompt=4,train_batch_size=2) it passes —8 >= 6— while the stride is3, so the batch still truncates to zero.Since
tbs * n % lcm == 0is equivalent totbs % (lcm // gcd(lcm, n)) == 0, the stride is the quantity the truncation actually depends on, so this checks it directly:train_batch_size >= prompt_strideat startup. This is exactly the condition under which at least one prompt survives truncation, and it subsumes the previous assert. The message reportspolicy_dp_size/ref_dp_size/lcm_dp_size/n_samples_per_promptand the stride to settrain_batch_sizeto.train_batch_sizeis at or above the stride but not a multiple of it. That case is not fatal, but it silently drops the same prompts from every training batch, so the effective batch size is smaller than configured._remove_tail_dataas well.validate_batch_sizesonly seestrain_batch_size, while the truncation also runs on batches sized by the caller (examples/train/async/async_trainer.pyandskyrl-agent's trainer), so the guard has to sit where the stride is applied.Only the fatal case is a hard failure; the lossy-but-nonempty case stays a warning so that existing configs keep running.
Testing
test_remove_tail_data_rejects_batch_smaller_than_prompt_stridecovers the raise and that a batch at or above the stride still truncates to the largest multiple.test_validate_batch_sizescover the startup assert (using the placement from the issue) and the non-multiple warning.uv run --isolated --extra skyrl-train --extra dev pytest tests/train/ tests/backends/skyrl_train/ --ignore=tests/backends/skyrl_train/gpu -m "not vllm"— 1451 passed, 7 skipped.The pre-existing
test_validate_batch_sizes_lcm_dp_requirementpasses unchanged: it usesn_samples_per_prompt=1, where the stride equalslcm_dp_sizeand the new assert reduces to the old one.🤖 Generated with Claude Code