Repository navigation
[RL] Avoid full-vocab gather for TP policy statistics - #4685
Conversation
|
|
|
Thanks @tianyu-l -- I kept batch-invariant mode on the existing full-gather path and added a dedicated CPU regression test that pins the patched vLLM call to |
|
i've updated #4582, this should not affect your changes |
571b6ce to
72bc39a
Compare
|
@claude I'm confused by the input sharding contract of the target function. See my latest comments. Please explain. |
|
Claude finished @tianyu-l's task in 4m 20s —— View job Input sharding contract of
|
| Regime | tp_size |
global_vocab_size |
Expected logits |
Path |
|---|---|---|---|---|
| Trainer, opt-in (new) | >1 |
set | local shard [T, V_local] |
vocab-parallel CE + _VocabParallelEntropy, no gather |
| Trainer, legacy | >1 |
None |
local shard [T, V_local] |
gather S(-1)->I, then plain F.cross_entropy on full logits |
| Generator / vLLM patch | 1 |
None |
full [N, V] |
plain F.cross_entropy, no collectives |
So the answer to "is this function assuming vocab parallel or not?" is: only when a TP mesh is on the stack. The tp_size > 1 branches assume local shards; the tp_size == 1 fallthrough assumes full logits. This was already true before the PR -- the PR only adds the first row (a no-gather branch guarded by global_vocab_size is not None). When global_vocab_size is None, behavior is byte-identical to the old code, so the legacy contract is preserved, not violated.
Why the patched vLLM path is the tp_size == 1 (full-logits) branch
Two independent reasons, either one sufficient:
-
No active SPMD mesh around the sampler. The wrapper only enters a mesh context inside
compute_logits/forwardviawith self.spmd_context():(vllm_wrapper.py:491), and the mesh stack is pushed only inside that context manager (spmd_types.py:193). vLLM's v2 sampler calls the patchedcompute_token_logprobs(batch_invariance.py:59) downstream ofcompute_logits, outside anyspmd_context. Socurrent_spmd_mesh()isNone->spmd_mesh_size("tp") == 1-> the plain branch. The patched call also never passesglobal_vocab_size, so even the row-1 guard is off. -
The logits are already gathered to full vocab. You're right that
decoder_sharding.py:358keeps the LM head vocab-parallel even in inference. Butcompute_logitsgathersS(-1) -> Rbefore returning to vLLM (vllm_wrapper.py:496-505), exactly to satisfy the "vllm expects unsharded logits" comment you cited. So by the time the patched kernel runs, the tensor is full[N, V]-- matching what vLLM's nativecompute_token_logprobsassumes (thelogprob.py#L89behavior you pointed at).
Both facts agree: the patched replacement receives full, unsharded logits, and compute_logprobs correctly routes it through the non-vocab-parallel branch. Passing global_vocab_size there would be the actual bug -- it would select the sharded algorithm on already-gathered full logits and produce wrong reductions -- which is precisely why the batch-invariant path leaves it unset.
On "it used to assume so, but this code violates it"
The pre-PR compute_logprobs was not "always vocab parallel" either -- under tp_size > 1 it gathered S(-1) -> I and then ran plain CE, and under tp_size == 1 it ran plain CE directly. The PR keeps both of those unchanged and inserts an opt-in shard-local branch ahead of them. The input each caller must supply is unchanged for every pre-existing caller.
Suggested doc fix
The one thing worth improving is the docstring: it currently describes the algorithm but not the input contract. Given the confusion, it would help to state the shape contract explicitly per regime, e.g.:
logitsis the local vocab shard[T, V_local]when a TP mesh is active (global_vocab_sizethen selects gather-free vs. legacy-gather); it is the full[T, V](or[N, V]) tensor when no TP mesh is active, as in the vLLM generator path wherecompute_logitshas already all-gathered the vocab shards.
Add input-contract docs to compute_logprobs →
- Read
compute_logprobsandcross_entropy_lossinloss.py - Read
vllm_wrapper.pycompute_logitsgather - Read
batch_invariance.pypatching path - Read
spmd_types.pyto confirm the runtime mesh gate - Explain the contract and answer @tianyu-l
Summary: the contract is runtime-selected, not static. TP mesh active -> local shards; no TP mesh (generator/vLLM patch) -> full logits. The vLLM path hits the full-logits branch both because the sampler runs outside any spmd_context and because compute_logits already all-gathers to Replicate, so nothing is violated. Only a docstring clarification is recommended.
• branch feature/vocab-parallel-policy-stats
|
@claude check my latest comment. In particular
|
|
Claude finished @tianyu-l's task in 3m 11s —— View job Batch invariance under the patched logprob pathShort answer: the premise is inverted. Under the shipped batch-invariant configs, the trainer does not use Q1: Does the trainer use the vocab-parallel path in batch-invariant mode?No. The batch-invariant recipes deliberately leave
So in
The generator hits Q2: So why is batch invariance preserved -- is it the MAX all-reduce /
|
|
Addressed the latest review in b750d3f. GRPOLoss.Config now inherits DAPOLoss.Config instead of repeating global_vocab_size. compute_logprobs now names and documents the vocab-parallel selection explicitly, checks is_in_batch_invariant_mode(), and keeps the full-vocabulary gather path with a warning even if a recipe mistakenly sets global_vocab_size. I did not assert that every TP call must set it: batch-invariant TP depends on the existing unset-size gather path. The vLLM patch docstrings now state that its input is full logits and the size is intentionally omitted. A CPU regression covers the misconfigured batch-invariant case. Locally, the two runnable focused tests pass; ten optional-dependency cases skip. Formatting, AST, lint, docstring and spelling hooks pass. Project Pyrefly remains limited by missing local dependencies and an invalid configured search path; the targeted check reports only missing vLLM and the pre-existing torch.stack union diagnostic. The new-head Lint, CPU Unit Test, and GPU Unit Tests require maintainer workflow approval; only three Meta checks have passed on this head so far. |
|
Addressed the latest review in Added regression coverage for both runtime branches and expanded recipe checks. Focused result: 3 passed, 13 optional-dependency cases skipped. Formatting, AST, flake8, docstring, spelling, and link hooks pass. The broader |
|
Also addressed the older test-placement threads in |
tianyu-l
left a comment
There was a problem hiding this comment.
please address last comment
| return grad_logits, None, None, None, None | ||
|
|
||
|
|
||
| class _VocabParallelEntropy(torch.autograd.Function): |
There was a problem hiding this comment.
can we align the namings of vocab parallel and loss parallel? I think the _LossParallelCrossEntropy here is also vocab parallel CE?
furthermore, can we unify _VocabParallelEntropy and _LossParallelCrossEntropy? like have a return entropy flag in _LossParallelCrossEntropy, is this possible?
There was a problem hiding this comment.
I think it's easier if we do this in a separate PR, as this PR got a lot of back-n-forth already. So I'm merging
| "global_vocab_size is required for vocab-parallel policy " | ||
| "statistics" | ||
| ) | ||
| logprobs = -cross_entropy_loss( |
There was a problem hiding this comment.
this cross_entropy_loss is also checking tp_size > 1, can we deduplicate?
Summary
Why
The RL trainer previously gathered every vocabulary shard on every TP rank before computing sampled-token logprobs and entropy. The new path uses shard-local values plus per-token reductions for the global maximum, softmax denominator, target contribution, and entropy-weighted sum. Entropy remains a no-grad metric.
Correctness and compatibility
Distributed parity tests compare logprobs, entropy, and non-uniformly weighted gradients against the full-vocabulary reference. They cover TP=2/4, DP=2 x TP=2, even/uneven vocabulary, FP32/BF16, ignored labels, partially masked logits, and strict SPMD type checking.
Batch-invariant training leaves
global_vocab_sizeunset to retain the original trainer operation sequence. In the generator,VLLMModelWrapper.compute_logits()already gathers the LM-head vocabulary shards into the full logits tensor expected by vLLM, so the patched vLLM logprob call correctly receives full logits and does not select the trainer vocab-sharded path. A CPU regression test pins itscompute_logprobs(logits, labels)call and multi-column token-id behavior. The generic RL loss default also remains unset, preserving the fallback for configurations that do not opt in. A runtimeis_in_batch_invariant_mode()check forces that same gather path if a recipe accidentally suppliesglobal_vocab_size; the selector is documented explicitly incompute_logprobs.GRPOLoss.Confignow inherits the shared DAPO config instead of duplicating this field.Recipe configuration regression tests assert that ordinary recipes use the model vocabulary size and batch-invariant recipes leave it unset. They are wired into the RL CI workflow, which installs vLLM and the example dependencies.
Performance evidence
A local forward+backward CPU/Gloo component benchmark with the Qwen3 vocabulary size (151,936) measured 2.66x at TP2 and 3.97x at TP4 after warmup. These are component-level CPU measurements, not GPU or end-to-end RL throughput claims.
Validation
torch.stackunion diagnostic, with no new diagnostic from the guard; full-project checking is unavailable because this environment lacks nightly PyTorch, Monarch, TorchStore, Renderers, and the configured PyTorch search pathThe local stable PyTorch environment cannot collect the mainline loss tests because it lacks the nightly
DataParallelMeshDimsAPI. Ten config tests skip locally for missing optional vLLM/renderers dependencies. The new-head Lint, CPU Unit Test, and GPU Unit Tests workflows are awaiting maintainer approval (action_required); only three Meta checks have passed on this head. Prior-head CI results are not being claimed for this revision. This change is orthogonal to the loss abstraction work in #4582.Closes #4683