Repository navigation
Conversation
…-actor comparison Fixes verl-project#4162 ## Problem When calculating the `rollout_actor_probs_pearson_corr` metric, there was a mismatch in how log probabilities were computed: - VLLM rollout: Uses **unscaled** logits → `log_softmax(logits)` - Actor training: Uses **temperature-scaled** logits → `log_softmax(logits/T)` This caused the Pearson correlation coefficient to deviate from 1.0 even when the model was perfectly consistent, making the metric unreliable for debugging training-inference alignment. ## Solution ### 1. Megatron Actor (`verl/workers/actor/megatron_actor.py`) - Compute `log_probs_for_metrics` using unscaled logits before applying temperature - Keep original `log_probs` with temperature scaling for training ### 2. FSDP Actor (`verl/workers/actor/dp_actor.py`) - Add `log_probs_for_metrics` computation in `_forward_micro_batch()` - Handle both rmpad and non-rmpad cases - Return 3 values: `(entropy, log_probs, log_probs_for_metrics)` ### 3. Metrics (`verl/utils/debug/metrics.py`) - Use `log_probs_for_metrics` if available, fallback to `old_log_probs` - Ensures accurate Pearson correlation when comparing with rollout ### 4. Tests (`tests/utils/test_issue_4162_fix.py`) - Verify temperature scaling causes metric deviation - Verify unscaled log_probs achieve perfect correlation - Test realistic training scenarios ## Impact - ✅ Fixes `rollout_actor_probs_pearson_corr` metric accuracy - ✅ No impact on training (temperature scaling still applied) - ✅ Backward compatible (falls back to old_log_probs if new field unavailable) ## Testing ```bash python -m pytest tests/utils/test_issue_4162_fix.py -v ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request correctly identifies and addresses an inconsistency in log probability computation that was affecting the rollout_actor_probs_pearson_corr metric. The proposed solution of using unscaled logits for metric calculation is sound, and the changes are implemented across both the Megatron and FSDP actors, with a fallback for backward compatibility. The addition of comprehensive unit tests is also a great step towards ensuring the correctness of this fix.
However, I've identified two critical issues that will cause runtime errors or prevent the fix from working as intended. One is a breaking change in dp_actor.py where a method's return signature was updated, but a call site was not, which will lead to an unpacking error. The other is an incomplete implementation in megatron_actor.py, where the newly computed metric data is not propagated correctly. I've also included a high-severity suggestion to simplify some convoluted logic for better maintainability.
| def _forward_micro_batch( | ||
| self, micro_batch, temperature, calculate_entropy=False | ||
| ) -> tuple[torch.Tensor, torch.Tensor]: | ||
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
There was a problem hiding this comment.
The change in the return signature of _forward_micro_batch from a 2-tuple to a 3-tuple introduces a breaking change. The call to this method within update_policy at line 484 has not been updated to handle the new signature, which will cause a ValueError: too many values to unpack (expected 2) at runtime.
To fix this, the call at line 484 should be updated to correctly unpack the three returned values:
entropy, log_prob, _ = self._forward_micro_batch(...)This will unpack log_probs_for_metrics and discard it, as it is not needed for policy updates.
| # Compute unscaled log_probs for metrics (to match VLLM rollout behavior) | ||
| # This ensures rollout_actor_probs_pearson_corr metric is accurate | ||
| # See: https://github.com/volcengine/verl/issues/4162 | ||
| log_probs_unscaled = vocab_parallel_log_probs_from_logits(logits.clone(), label) | ||
| log_probs_unscaled = log_probs_unscaled.masked_fill(~label_mask, 0.0) | ||
| ret["log_probs_for_metrics"] = log_probs_unscaled |
There was a problem hiding this comment.
While log_probs_for_metrics is correctly computed here and added to the ret dictionary, it is not propagated out of the compute_log_prob method. This makes the fix incomplete for MegatronPPOActor, and the rollout_actor_probs_pearson_corr metric will remain inaccurate when using it.
To complete the fix, you need to:
- Update
compute_logprobs_fn(defined insidecompute_log_prob) to extractlog_probs_for_metricsfrom theoutputdictionary, similar to how it extractslog_probs. - Update the
compute_log_probmethod to collect and returnlog_probs_for_metricsalongsidelog_probsandentropys, mirroring the changes made indp_actor.py.
| if not hasattr(self, 'use_fused_kernels') or not self.use_fused_kernels or 'log_probs_for_metrics' not in locals(): | ||
| if 'log_probs_for_metrics' not in locals(): | ||
| log_probs_for_metrics = log_probs |
There was a problem hiding this comment.
The conditional logic for the fallback assignment of log_probs_for_metrics is unnecessarily complex and can be simplified. The current nested if statements are confusing and harder to maintain. The outer condition is redundant because the logic can be reduced to a single check for whether log_probs_for_metrics was computed in the current scope (e.g., when not using fused kernels).
Simplifying this logic improves readability and reduces the risk of future bugs.
if 'log_probs_for_metrics' not in locals():
log_probs_for_metrics = log_probs|
Maybe we should let rollout return logprobs after apply temperature and top_k/top_p? @vermouth1992 @szrlee |
@wuxibin89 yes, processed_logprobs may be a better choice. |
|
@JobQiu Can you help contribute to change vLLM logprobs_mode to |
…5860) ### What does this PR do? Fixes #5859 When `response_mask` is all `False` (e.g., in extreme rejection sampling scenarios), `calculate_debug_metrics` crashes with: ``` RuntimeError: The size of tensor a (4864) must match the size of tensor b (310002) at non-singleton dimension 1 ``` ### Checklist Before Starting - [x] Search for similar PRs: - https://github.com/volcengine/verl/pulls?q=is%3Apr+calculate_debug_metrics - Found related PRs #5229 and #4252 which add/modify metrics but don't fix this crash - [x] Format the PR title as `[{modules}] {type}: {description}` ### Test Existing unit tests pass. This is a defensive bug fix for an edge case. ### API and Usage Example No API changes. The function signature remains the same: ```python from verl.utils.debug.metrics import calculate_debug_metrics # Normal case - works as before metrics = calculate_debug_metrics(data) # Returns: {'training/rollout_probs_diff_valid': 1, ...} # Edge case - now handled gracefully instead of crashing metrics = calculate_debug_metrics(data_with_all_false_mask) # Returns: {'training/rollout_probs_diff_valid': 0, 'training/rollout_probs_diff_max': nan, ...} ``` ### Design & Code Changes **File changed:** `verl/utils/debug/metrics.py` (+12 lines) **Root Cause:** ```python # Line 58-60 def calculate_log_prob_diff(log_probs1, log_probs2, mask): full_diff = torch.abs(log_probs1 - log_probs2) # RuntimeError when mask is all False! return torch.masked_select(full_diff, mask) ``` **Solution:** Add early return check at Line 102-111: ```python if not response_mask_bool.any(): logger.warning("response_mask is all False, returning default metrics") return { "training/rollout_probs_diff_valid": 0, "training/rollout_probs_diff_max": float("nan"), "training/rollout_probs_diff_mean": float("nan"), "training/rollout_probs_diff_std": float("nan"), "training/rollout_actor_probs_pearson_corr": float("nan"), } ``` ### Checklist Before Submitting - [x] Read the [Contribute Guide](https://github.com/volcengine/verl/blob/main/CONTRIBUTING.md) - [x] Apply pre-commit checks (ruff check & format passed locally) - [x] Add / Update documentation (N/A - internal bug fix) - [x] Add unit or end-to-end test(s) (N/A - edge case, existing tests pass) - [ ] Send a message in the `ci-request` channel (will do after CI passes) Co-authored-by: jackiemac <jackiemac@Zifengs-MacBook-Air.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…erl-project#5860) ### What does this PR do? Fixes verl-project#5859 When `response_mask` is all `False` (e.g., in extreme rejection sampling scenarios), `calculate_debug_metrics` crashes with: ``` RuntimeError: The size of tensor a (4864) must match the size of tensor b (310002) at non-singleton dimension 1 ``` ### Checklist Before Starting - [x] Search for similar PRs: - https://github.com/volcengine/verl/pulls?q=is%3Apr+calculate_debug_metrics - Found related PRs verl-project#5229 and verl-project#4252 which add/modify metrics but don't fix this crash - [x] Format the PR title as `[{modules}] {type}: {description}` ### Test Existing unit tests pass. This is a defensive bug fix for an edge case. ### API and Usage Example No API changes. The function signature remains the same: ```python from verl.utils.debug.metrics import calculate_debug_metrics # Normal case - works as before metrics = calculate_debug_metrics(data) # Returns: {'training/rollout_probs_diff_valid': 1, ...} # Edge case - now handled gracefully instead of crashing metrics = calculate_debug_metrics(data_with_all_false_mask) # Returns: {'training/rollout_probs_diff_valid': 0, 'training/rollout_probs_diff_max': nan, ...} ``` ### Design & Code Changes **File changed:** `verl/utils/debug/metrics.py` (+12 lines) **Root Cause:** ```python # Line 58-60 def calculate_log_prob_diff(log_probs1, log_probs2, mask): full_diff = torch.abs(log_probs1 - log_probs2) # RuntimeError when mask is all False! return torch.masked_select(full_diff, mask) ``` **Solution:** Add early return check at Line 102-111: ```python if not response_mask_bool.any(): logger.warning("response_mask is all False, returning default metrics") return { "training/rollout_probs_diff_valid": 0, "training/rollout_probs_diff_max": float("nan"), "training/rollout_probs_diff_mean": float("nan"), "training/rollout_probs_diff_std": float("nan"), "training/rollout_actor_probs_pearson_corr": float("nan"), } ``` ### Checklist Before Submitting - [x] Read the [Contribute Guide](https://github.com/volcengine/verl/blob/main/CONTRIBUTING.md) - [x] Apply pre-commit checks (ruff check & format passed locally) - [x] Add / Update documentation (N/A - internal bug fix) - [x] Add unit or end-to-end test(s) (N/A - edge case, existing tests pass) - [ ] Send a message in the `ci-request` channel (will do after CI passes) Co-authored-by: jackiemac <jackiemac@Zifengs-MacBook-Air.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…erl-project#5860) ### What does this PR do? Fixes verl-project#5859 When `response_mask` is all `False` (e.g., in extreme rejection sampling scenarios), `calculate_debug_metrics` crashes with: ``` RuntimeError: The size of tensor a (4864) must match the size of tensor b (310002) at non-singleton dimension 1 ``` ### Checklist Before Starting - [x] Search for similar PRs: - https://github.com/volcengine/verl/pulls?q=is%3Apr+calculate_debug_metrics - Found related PRs verl-project#5229 and verl-project#4252 which add/modify metrics but don't fix this crash - [x] Format the PR title as `[{modules}] {type}: {description}` ### Test Existing unit tests pass. This is a defensive bug fix for an edge case. ### API and Usage Example No API changes. The function signature remains the same: ```python from verl.utils.debug.metrics import calculate_debug_metrics # Normal case - works as before metrics = calculate_debug_metrics(data) # Returns: {'training/rollout_probs_diff_valid': 1, ...} # Edge case - now handled gracefully instead of crashing metrics = calculate_debug_metrics(data_with_all_false_mask) # Returns: {'training/rollout_probs_diff_valid': 0, 'training/rollout_probs_diff_max': nan, ...} ``` ### Design & Code Changes **File changed:** `verl/utils/debug/metrics.py` (+12 lines) **Root Cause:** ```python # Line 58-60 def calculate_log_prob_diff(log_probs1, log_probs2, mask): full_diff = torch.abs(log_probs1 - log_probs2) # RuntimeError when mask is all False! return torch.masked_select(full_diff, mask) ``` **Solution:** Add early return check at Line 102-111: ```python if not response_mask_bool.any(): logger.warning("response_mask is all False, returning default metrics") return { "training/rollout_probs_diff_valid": 0, "training/rollout_probs_diff_max": float("nan"), "training/rollout_probs_diff_mean": float("nan"), "training/rollout_probs_diff_std": float("nan"), "training/rollout_actor_probs_pearson_corr": float("nan"), } ``` ### Checklist Before Submitting - [x] Read the [Contribute Guide](https://github.com/volcengine/verl/blob/main/CONTRIBUTING.md) - [x] Apply pre-commit checks (ruff check & format passed locally) - [x] Add / Update documentation (N/A - internal bug fix) - [x] Add unit or end-to-end test(s) (N/A - edge case, existing tests pass) - [ ] Send a message in the `ci-request` channel (will do after CI passes) Co-authored-by: jackiemac <jackiemac@Zifengs-MacBook-Air.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…erl-project#5860) ### What does this PR do? Fixes verl-project#5859 When `response_mask` is all `False` (e.g., in extreme rejection sampling scenarios), `calculate_debug_metrics` crashes with: ``` RuntimeError: The size of tensor a (4864) must match the size of tensor b (310002) at non-singleton dimension 1 ``` ### Checklist Before Starting - [x] Search for similar PRs: - https://github.com/volcengine/verl/pulls?q=is%3Apr+calculate_debug_metrics - Found related PRs verl-project#5229 and verl-project#4252 which add/modify metrics but don't fix this crash - [x] Format the PR title as `[{modules}] {type}: {description}` ### Test Existing unit tests pass. This is a defensive bug fix for an edge case. ### API and Usage Example No API changes. The function signature remains the same: ```python from verl.utils.debug.metrics import calculate_debug_metrics # Normal case - works as before metrics = calculate_debug_metrics(data) # Returns: {'training/rollout_probs_diff_valid': 1, ...} # Edge case - now handled gracefully instead of crashing metrics = calculate_debug_metrics(data_with_all_false_mask) # Returns: {'training/rollout_probs_diff_valid': 0, 'training/rollout_probs_diff_max': nan, ...} ``` ### Design & Code Changes **File changed:** `verl/utils/debug/metrics.py` (+12 lines) **Root Cause:** ```python # Line 58-60 def calculate_log_prob_diff(log_probs1, log_probs2, mask): full_diff = torch.abs(log_probs1 - log_probs2) # RuntimeError when mask is all False! return torch.masked_select(full_diff, mask) ``` **Solution:** Add early return check at Line 102-111: ```python if not response_mask_bool.any(): logger.warning("response_mask is all False, returning default metrics") return { "training/rollout_probs_diff_valid": 0, "training/rollout_probs_diff_max": float("nan"), "training/rollout_probs_diff_mean": float("nan"), "training/rollout_probs_diff_std": float("nan"), "training/rollout_actor_probs_pearson_corr": float("nan"), } ``` ### Checklist Before Submitting - [x] Read the [Contribute Guide](https://github.com/volcengine/verl/blob/main/CONTRIBUTING.md) - [x] Apply pre-commit checks (ruff check & format passed locally) - [x] Add / Update documentation (N/A - internal bug fix) - [x] Add unit or end-to-end test(s) (N/A - edge case, existing tests pass) - [ ] Send a message in the `ci-request` channel (will do after CI passes) Co-authored-by: jackiemac <jackiemac@Zifengs-MacBook-Air.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…erl-project#5860) ### What does this PR do? Fixes verl-project#5859 When `response_mask` is all `False` (e.g., in extreme rejection sampling scenarios), `calculate_debug_metrics` crashes with: ``` RuntimeError: The size of tensor a (4864) must match the size of tensor b (310002) at non-singleton dimension 1 ``` ### Checklist Before Starting - [x] Search for similar PRs: - https://github.com/volcengine/verl/pulls?q=is%3Apr+calculate_debug_metrics - Found related PRs verl-project#5229 and verl-project#4252 which add/modify metrics but don't fix this crash - [x] Format the PR title as `[{modules}] {type}: {description}` ### Test Existing unit tests pass. This is a defensive bug fix for an edge case. ### API and Usage Example No API changes. The function signature remains the same: ```python from verl.utils.debug.metrics import calculate_debug_metrics # Normal case - works as before metrics = calculate_debug_metrics(data) # Returns: {'training/rollout_probs_diff_valid': 1, ...} # Edge case - now handled gracefully instead of crashing metrics = calculate_debug_metrics(data_with_all_false_mask) # Returns: {'training/rollout_probs_diff_valid': 0, 'training/rollout_probs_diff_max': nan, ...} ``` ### Design & Code Changes **File changed:** `verl/utils/debug/metrics.py` (+12 lines) **Root Cause:** ```python # Line 58-60 def calculate_log_prob_diff(log_probs1, log_probs2, mask): full_diff = torch.abs(log_probs1 - log_probs2) # RuntimeError when mask is all False! return torch.masked_select(full_diff, mask) ``` **Solution:** Add early return check at Line 102-111: ```python if not response_mask_bool.any(): logger.warning("response_mask is all False, returning default metrics") return { "training/rollout_probs_diff_valid": 0, "training/rollout_probs_diff_max": float("nan"), "training/rollout_probs_diff_mean": float("nan"), "training/rollout_probs_diff_std": float("nan"), "training/rollout_actor_probs_pearson_corr": float("nan"), } ``` ### Checklist Before Submitting - [x] Read the [Contribute Guide](https://github.com/volcengine/verl/blob/main/CONTRIBUTING.md) - [x] Apply pre-commit checks (ruff check & format passed locally) - [x] Add / Update documentation (N/A - internal bug fix) - [x] Add unit or end-to-end test(s) (N/A - edge case, existing tests pass) - [ ] Send a message in the `ci-request` channel (will do after CI passes) Co-authored-by: jackiemac <jackiemac@Zifengs-MacBook-Air.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…for rollout vs actor (#8025) ### What does this PR do? Adds three raw log-prob metrics next to the existing `training/rollout_probs_diff_*`: - `training/rollout_logprobs_mismatch_count`: valid response tokens whose `rollout_log_probs != old_log_probs` - `training/rollout_logprobs_diff_max` / `_mean`: |Δ| of the raw log-probs `rollout_probs_diff_*` compares `exp(log_prob)` and cannot see a 1-ULP log-prob difference (float32 `-0.10000000149` vs `-0.10000000894` give the same prob). Exact 0-diff train-rollout work (#7688, verl-project/verl-omni#637) needs a gate that only passes at zero elements, so the count is exposed directly. In the V1 trainer the count is aggregated by sum, not mean. Emitted from `calculate_debug_metrics`, so every existing caller (v1 trainer, legacy trainer, separation trainer) gets it under `rollout.calculate_log_probs=True` with no config change. ### Checklist Before Starting - [x] Similar PRs: https://github.com/verl-project/verl/pulls?q=is%3Apr+is%3Aopen+rollout_probs_diff — #6033 renames a helper, #4252 changes the pearson input; neither exposes raw log-prob metrics. #7196 (closed) was a bypass-mode diagnostic. - [x] Title format `[training_utils, trainer, tests] feat: ...` ### Test ```bash pytest tests/utils/debug/test_metrics.py tests/trainer/ppo/v1/test_metrics_aggregator_on_cpu.py -q # 23 passed ``` New cases: 1-ULP float32 pair where `rollout_probs_diff_max == 0` but `mismatch_count == 1`; all-masked batch returns `valid=0, mismatch_count=0`; aggregator sums the count across iterations. ### API and Usage Example No API change. Three new keys in the metrics dict when `actor_rollout_ref.rollout.calculate_log_probs=True`. ### Design & Code Changes - `verl/utils/debug/metrics.py`: compute the raw comparison alongside the prob-based one; default keys on an all-masked batch. - `verl/trainer/ppo/v1/utils.py`: `rollout_logprobs_mismatch_count` uses the `sum` aggregation rule. - tests as above. ### Checklist Before Submitting - [x] Read the Contribute Guide. - [x] `pre-commit run --files <changed files>`: ruff, ruff format, mypy, license, docstring, structure checks pass. - [ ] Documentation: docstring only; no docs page lists per-metric keys for this file. - [x] Unit tests added (CPU). AI assistance was used for this change. I reviewed every line and ran the tests. Signed-off-by: cr-gao <gaochenrui@sjtu.edu.cn>
Summary
Fixes #4162
This PR ensures the
rollout_actor_probs_pearson_corrmetric accurately reflects the correlation between rollout and actor probabilities by computing log probabilities from unscaled logits for metrics calculation.Problem
Currently, there's an inconsistency in log probability computation:
log_softmax(logits)log_softmax(logits/temperature)Since
log_softmax(logits) ≠ log_softmax(logits/T)when temperature ≠ 1.0, therollout_actor_probs_pearson_corrmetric becomes inaccurate, misleading users about the actual alignment between rollout and actor behaviors.Solution
Megatron Actor: Added
log_probs_for_metricscomputation before temperature scaling inlogits_processor()(verl/workers/actor/megatron_actor.py:574-602)FSDP Actor: Modified
_forward_micro_batch()to return unscaled log probabilities alongside the scaled version (verl/workers/actor/dp_actor.py)Metrics: Updated
calculate_debug_metrics()to prioritizelog_probs_for_metricswhen available, falling back toold_log_probsfor backward compatibility (verl/utils/debug/metrics.py:86-93)Key Design Decisions
old_log_probsiflog_probs_for_metricsis not availableold_log_probsunchanged)Testing
Added comprehensive unit tests in
tests/utils/test_issue_4162_fix.py:test_temperature_scaling_causes_pearson_deviation- Demonstrates the original problemtest_unscaled_logprobs_achieve_perfect_correlation- Verifies the fix achieves perfect correlationtest_temperature_scaling_mathematical_inequality- Proves mathematical difference between scaled/unscaledtest_temperature_one_no_difference- Sanity check for temperature=1.0test_realistic_training_scenario- Full scenario with 256 samplesImpact