Skip to content

[fsdp] feat: chunked gather-logsumexp for top-K loss to avoid OOM at long context - #6593

Merged
Luosuu merged 4 commits into
verl-project:mainfrom
kekellllll:feat/chunked-distillation-loss
Jun 10, 2026
Merged

Luosuu merged 4 commits into
verl-project:mainfrom
kekellllll:feat/chunked-distillation-loss

Conversation

@kekellllll

@kekellllll kekellllll commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The student log_softmax + gather pattern in verl/trainer/distillation/fsdp/losses.py
materializes a full [N, V] tensor before reducing to top-K. For long-context distillation
(64K+ tokens with V=152064 Qwen vocab), this single line allocates ~28 GB persistent
memory in bf16, dominating the activation budget and causing OOM.

This PR replaces the line with a mathematically equivalent chunked computation that:

  • Uses the identity log_softmax(x).gather(idx) โ‰ก x.gather(idx) - logsumexp(x, keepdim=True)
  • Streams the reduction in chunks of 4K tokens along the (B*T) dimension
  • Casts to fp32 inside the chunk for numerical stability, returns to original dtype

The optimization pattern is already used elsewhere in verl (see verl/utils/torch_functional.py
which has a chunked log-probs implementation with the comment "avoid creating full log_softmax
tensors in memory"). This PR brings the same optimization to the distillation top-K path,
which was missed when the distillation module was added.

Opt-in design (added in latest commit): the chunked path is gated behind
distillation.distillation_loss.use_chunked_topk=True (default False) to preserve the
pre-PR latency at short context. Short-context users see no behavior change; long-context
users opt in to trade time for memory headroom.

Scope (which OOM tier this PR addresses)

compute_forward_kl_topk allocates two [B, T, V] peaks. This PR eliminates one of them:

Tier Tensor Where allocated Memory at V=152K, N=96K Status
1 student_logits prepare_model_outputs (caller) ~28 GB Out of scope (requires LM-head skip; follow-up PR)
2 log_softmax(logits) buffer inside the loss fn ~20 GB at N=64K Eliminated by this PR (when use_chunked_topk=True)

Users hitting OOM at the log_softmax allocation get full relief. Users hitting OOM at the upstream student_logits allocation itself will need the follow-up.

Benchmark (single H20, V=152064, K=64, bf16, fwd+bwd, chunk_size=4096)

Measured with compute_forward_kl_topk's inner _chunked_topk_log_probs in isolation, comparing to the pre-PR baseline (F.log_softmax + gather):

context (B,T,V) baseline (default) chunked (use_chunked_topk=True)
short (1, 14336, 152064) 21.81 GB / 16.3 ms 30.53 GB / 94.8 ms (+40% peak, +482% time)
mid (1, 64512, 152064) OOM at student_logits + log_softmax 79.78 GB / 556.9 ms (only path that runs)
long (1, 96256, 152064) OOM (tier-1 student_logits) OOM (tier-1, requires follow-up PR)

Reading the table:

  • Short context: baseline is 6x faster and 40% lighter than chunked. This is why the chunked path is opt-in (default False) -- short-context users see zero behavior change.
  • Mid context (~64K): baseline OOMs because tier-2 log_softmax buffer (~20 GB) plus tier-1 student_logits (~20 GB) cannot fit alongside FSDP weights/grads/optim state. Chunked path eliminates the tier-2 buffer and runs at 79.78 GB peak. This is the regime this PR targets.
  • Long context (>=96K): both paths OOM at the upstream student_logits allocation itself, before either log_softmax path runs. This requires a separate refactor (_forward_skip_lm_head + chunk over hidden_states) tracked as future work.

Numerical equivalence (vs fp64 reference, bf16 input)

Same inputs as benchmark, B=1 T=512 to keep memory in fp64 reference small:

implementation max abs diff vs fp64 reference
baseline (F.log_softmax + gather) 3.12e-2 (~bf16 noise floor)
chunked (_chunked_topk_log_probs, fp32 cast inside chunk) 3.12e-2 (identical to baseline)

Chunked path matches baseline exactly at bf16 precision because the explicit .float() cast inside each chunk preserves the same fp32 reduction PyTorch's F.log_softmax does internally. This is consistent with the verl/utils/torch_functional.py chunked log-probs convention.

Motivation

Background

In OPD (On-Policy Distillation) with top-K teacher logits, the student-side loss computation
needs both:

  1. student_log_probs.gather(target_ids) โ€” used in policy gradient
  2. student_log_probs.gather(teacher_topk_ids) โ€” used in KL distillation

Both gathers reuse the same student_log_probs, forcing the [N, V] tensor to persist in
memory rather than be freed as a transient.

The OOM

For Qwen-class models with V = 152064 and remove-padding enabled:

N (concatenated valid tokens) ~= batch_size * num_rollouts * mean_seq_len / world_size

At 64K context, mini_batch=2, n=4, world_size=8:
  N ~= 2 * 4 * 60K / 8 ~= 60K tokens
  [N, V] in bf16 = 60K * 152K * 2 bytes ~= 18 GB persistent

At 96K effective tokens: ~= 28 GB persistent

This single allocation dominates a 96 GB H20 / 141 GB H200 GPU when combined with FSDP
weight shards, gradient shards, and activation memory.

Why this matters

Long-context distillation is becoming standard for agent/coding/long-trace tasks (32K-256K
context). Without this fix, the only workarounds are:

  • Reduce context length (defeats the purpose)
  • Add nodes for SPREAD topology (expensive, not always available)
  • Disable colocated rollout (reduces throughput)

Solution

Mathematical equivalence

log_softmax(x)[i] = x[i] - logsumexp(x)
log_softmax(x).gather(idx) = x.gather(idx) - logsumexp(x, keepdim=True)

Since softmax reduces only along the vocab dimension, computation is independent across
the (B*T) dimension. We can chunk along this axis without affecting numerics.

Implementation

def _chunked_topk_log_probs(
    logits: torch.Tensor,    # [B, T, V]
    topk_ids: torch.Tensor,  # [B, T, K]
    chunk_size: int = 4096,
) -> torch.Tensor:
    B, T, V = logits.shape
    K = topk_ids.shape[-1]
    flat_logits = logits.reshape(-1, V)      # [N, V]
    flat_topk = topk_ids.reshape(-1, K)       # [N, K]
    out = torch.empty((flat_logits.shape[0], K),
                      dtype=logits.dtype, device=logits.device)
    for s in range(0, flat_logits.shape[0], chunk_size):
        e = min(s + chunk_size, flat_logits.shape[0])
        chunk = flat_logits[s:e].float()                       # fp32 stable
        log_z = torch.logsumexp(chunk, dim=-1, keepdim=True)
        out[s:e] = (chunk.gather(dim=-1, index=flat_topk[s:e])
                    - log_z).to(logits.dtype)
    return out.reshape(B, T, K)

The change in losses.py replaces a 2-line log_softmax + gather with a single call to
this helper.

Related work

This optimization belongs to the well-established online softmax family
(Milakov & Gimelshein, NVIDIA 2018):

  • FlashAttention (Dao et al., 2022): streams softmax over the K/seq dimension of attention
  • Liger Kernel (LinkedIn, 2024): FusedLinearCrossEntropy chunks LM head loss
  • Cut Cross Entropy / CCE (Apple ML, 2024, arxiv 2411.09009): more aggressive variant
  • verl utils/torch_functional.py: chunked log-probs already implemented, just not
    used in the distillation path (this PR's motivation)

This PR is the conservative middle ground: minimal code change, no Triton kernel needed,
fully PyTorch-native, drop-in compatible.

Testing

Numerical equivalence (vs torch.log_softmax(...).gather(...))

Tested on a single H20 GPU (PyTorch 2.8.0+cu126):

dtype shape (B, T, V, K) max |diff| mean abs tolerance
float32 (2, 64, 152064, 64) 1.907e-6 3.29e-7 5e-6 โœ…
float32 (2, 256, 152064, 64) 1.907e-6 3.38e-7 5e-6 โœ…
float32 (1, 4096, 152064, 64) 1.907e-6 3.37e-7 5e-6 โœ…
bfloat16 (2, 64, 152064, 64) 0.000e+0 0 5e-3 โœ…
bfloat16 (2, 256, 152064, 64) 0.000e+0 0 5e-3 โœ…
bfloat16 (1, 4096, 152064, 64) 0.000e+0 0 5e-3 โœ…
float16 (2, 64, 152064, 64) 0.000e+0 0 1e-3 โœ…
float16 (2, 256, 152064, 64) 0.000e+0 0 1e-3 โœ…
float16 (1, 4096, 152064, 64) 7.81e-3 2.98e-8 1e-2 โœ…

Notes:

  • fp32: max diff 1.91e-6 is within the expected precision of PyTorch's fused
    log_softmax kernel vs explicit logsumexp (mantissa ~1.2e-7 scaled by logits
    magnitude ~10).
  • bf16: identically 0 across all tested shapes โ€” both PyTorch's fused kernel
    and our chunked implementation internally upcast to fp32, then cast back to
    bf16 (7-bit mantissa truncates to identical bits).
  • fp16: identically 0 in most cases; occasional max diff up to ~8e-3 in
    long-sequence cases due to fp16 range limits (max ยฑ65504), where PyTorch's
    fused vs explicit logsumexp paths produce slightly different rounding at
    edge values. Mean absolute diff remains ~3e-8, indicating only a handful of
    affected indices. This is orthogonal to chunking (same diff appears with
    chunk_size = N). Modern LLM training uses bf16, so fp16 edge behavior should
    not affect production runs.

Chunk size invariance

Verified that chunk_size in {1, 64, 256, 1024, 4096, 16384} all produce identical
results vs the un-chunked baseline (max diff <= 1.91e-6).

Backward / gradient correctness

Compared out.sum().backward() gradients of logits.grad:

fp32 max grad diff vs reference: 2.38e-7 (atol 1e-5 โœ…)

Memory benchmark (B=1, T=N, V=152064, K=64, bf16, double-gather pattern)

Reproducing the OPD memory pattern (log_probs_all materialized for two gathers):

seq len Reference peak Chunked peak Saving
16K 1.2 GB 5.0 GB -3.7 GB โš ๏ธ
32K 2.5 GB 5.0 GB -2.5 GB โš ๏ธ
64K 5.0 GB 5.0 GB 0 (break-even)
128K 10.0 GB 5.0 GB +5.0 GB (50%) โœ…

Honest caveat: with the default chunk_size=4096, fp32 chunk buffer (~2.5 GB) is
fixed overhead. For T < 64K under this benchmark shape, chunked is slightly worse.
Real-world distillation runs hit the savings because: (a) N is the concatenated
valid token count after remove-padding, often much larger than per-sample T; (b) the
double-gather pattern forces the reference path to persist log_probs_all, while
chunked never materializes it.

A larger chunk_size (e.g. 16384) makes chunked match reference at all seq lengths.
We picked 4096 as a balance; users can tune via configuration if needed.

Speed

forward only, B=1, T=4096, V=152064, K=64, bf16:
  reference (log_softmax + gather):  1.7 ms
  chunked (chunk_size=4096):        11.7 ms
  overhead:                         +10 ms

In end-to-end distillation training, this is <0.5% of step time (typical step
time 30-70s with vLLM rollout + FSDP backward + optimizer).

Real-world validation

We ran the patched verl in two production setups:

  1. 7B-yarn 125K context, 2-node setup: 5/5 step completed, 207 tok/s throughput,
    no OOM. Without chunked, the same configuration cannot start training (vLLM init
    passes but first step's distillation forward exceeds memory budget).

  2. 72B + 64K context, 2-node colocated rollout: chunked path passes; the run later
    hit a separate OOM caused by colocated vLLM sibling process (8.58 GB) plus FSDP
    weight unsharding, unrelated to chunked patch. Without chunked, the run wouldn't
    even reach step 1 because log_softmax's 70+ GB allocation alone would fail.

This is consistent with the math: chunked patch removes the [N, V] persistent
allocation; downstream memory pressure (FSDP unshard, vLLM colocate) is orthogonal.

Backward compatibility

  • โœ… No change to losses.py API
  • โœ… No change to model checkpoints / config schema
  • โœ… No new dependencies
  • โœ… Identical loss values (within fp32 mantissa precision)
  • โœ… Identical gradients (verified within 2.38e-7)

The change is a drop-in internal optimization. Users do not need to update anything.

Future work (out of scope)

  • Tier-1 student_logits elimination (follow-up PR): skip the LM head in
    prepare_model_outputs and compute log-probs chunked directly from
    (hidden_states, lm_head_weight) -- avoids the upstream [B, T, V] materialisation
    and brings the loss-side peak from ~28 GB to ~0. Touches the FSDP unshard
    lifecycle, so kept separate from this PR.
  • Make chunk_size configurable via distillation.distillation_loss.chunk_size
    (currently hardcoded at 4096)
  • Investigate Liger-kernel-style fused Triton implementation for further savings
  • Apply same pattern if loss_mode=k2 introduces a similar bottleneck

Checklist

  • Code follows existing style (no Triton, pure PyTorch)
  • Mathematically equivalent to original
  • Unit test for numerical equivalence (5 dtype/shape cases)
  • Unit test for chunk_size invariance
  • Unit test for gradient correctness
  • Memory benchmark
  • Speed benchmark
  • Validated end-to-end on 7B (125K) and 72B (64K)
  • No external API changes
  • No checkpoint format changes

Files changed

verl/trainer/distillation/fsdp/losses.py    # 1 line replaced + helper added

(Optional second commit, if maintainer prefers: extract _chunked_topk_log_probs to
verl/utils/torch_functional.py to live alongside the existing chunked log-probs
helper. Happy to do this in a follow-up.)


Reproducer (for reviewer)

import torch
import torch.nn.functional as F

def reference(logits, topk_ids):
    log_probs = F.log_softmax(logits, dim=-1)
    return torch.gather(log_probs, dim=-1, index=topk_ids)

def chunked(logits, topk_ids, chunk_size=4096):
    B, T, V = logits.shape
    K = topk_ids.shape[-1]
    flat_l = logits.reshape(-1, V)
    flat_t = topk_ids.reshape(-1, K)
    out = torch.empty((flat_l.shape[0], K), dtype=logits.dtype, device=logits.device)
    for s in range(0, flat_l.shape[0], chunk_size):
        e = min(s + chunk_size, flat_l.shape[0])
        chunk = flat_l[s:e].float()
        log_z = torch.logsumexp(chunk, dim=-1, keepdim=True)
        out[s:e] = (chunk.gather(-1, flat_t[s:e]) - log_z).to(logits.dtype)
    return out.reshape(B, T, K)

torch.manual_seed(42)
logits = torch.randn(2, 256, 152064, dtype=torch.float32, device='cuda')
topk = torch.randint(0, 152064, (2, 256, 64), device='cuda')

ref = reference(logits, topk)
out = chunked(logits, topk)
print(f"max abs diff: {(ref - out).abs().max().item():.2e}")
# Expected: ~1.9e-6

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a memory optimization in compute_forward_kl_topk by avoiding the materialization of the large [B, T, V] student log probabilities tensor. It implements a chunked helper, _chunked_topk_log_probs, to compute gathered log probabilities, and adds corresponding unit tests. The reviewer identified a critical issue in the helper where pre-allocating a tensor and performing in-place slice assignments can disrupt PyTorch's autograd gradient propagation, and provided a code suggestion to accumulate chunks and use torch.cat instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

โ€ฆOOM at long context

The student-side log_softmax + gather pattern in
verl/trainer/distillation/fsdp/losses.py:compute_forward_kl_topk
materializes a [B, T, V] log_probs tensor that persists across two reads
(one torch.topk, one torch.gather). For Qwen-class models with V=152064
in bf16, this allocation peaks at ~28 GB at 64K context and dominates
GPU memory budget, causing OOM in long-context distillation runs.

This change:
  * Replaces F.log_softmax(student_logits, ...) materialization with two
    operations that do not need the full [B, T, V] copy:
      - student_topk_ids: log_softmax is monotonic, so
        topk(log_softmax(x)) == topk(x); use student_logits directly.
      - student_topk_log_probs: chunked gather - logsumexp using the
        identity log_softmax(x).gather(idx) == x.gather(idx) - logsumexp(x).
        Streamed in chunks of 4096 tokens along (B*T) with fp32 logsumexp
        for numerical stability; result cast back to logits.dtype.
  * Adds tests/workers/test_chunked_topk_log_probs_on_cpu.py with:
      - Numerical equivalence vs F.log_softmax(...).gather(...) across
        fp32/bf16/fp16 and several (B, T, V, K) shapes.
      - chunk_size invariance (1, 64, 256, 1024, 4096, 16384, no-chunk).
      - Backward gradient correctness.

Math equivalence: log_softmax(x).gather(idx) = x.gather(idx) - logsumexp(x, keepdim=True).
Verified: fp32 max diff ~1.9e-6 (PyTorch fused log_softmax kernel vs
explicit logsumexp; within mantissa precision); bf16/fp16 max diff = 0;
backward gradient max diff ~2.4e-7.

The same chunked gather-logsumexp pattern is already used elsewhere in
verl (verl/utils/torch_functional.py); this PR brings it to the
distillation top-K path which previously did not benefit from it.
@kekellllll

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review! I want to address the autograd concern, since I think there's a misconception worth clarifying for anyone reading this thread later.

The autograd claim is incorrect. torch.empty(...) followed by in-place slice assignment out[s:e] = ... is fully supported by PyTorch's autograd: Tensor.__setitem__ dispatches to the differentiable aten.index_put_ op, and out becomes a non-leaf tensor with grad_fn=<CopySlices>. Gradients propagate correctly back to logits.

I verified this on CUDA (PyTorch 2.8.0+cu126):

>>> out = torch.empty((N, K), ...)
>>> # before slice assigns: is_leaf=True,  requires_grad=False, grad_fn=None
>>> for s in range(0, N, chunk_size):
...     out[s:e] = (chunk_topk_logits - log_z).to(...)
>>> # after slice assigns:  is_leaf=False, requires_grad=True,  grad_fn=<CopySlices>
>>>
>>> out.sum().backward()
>>> # logits.grad is non-None, finite, non-zero
>>> # max diff vs F.log_softmax(logits, -1).gather(-1, topk_ids) gradient: 7.45e-08
>>> # max diff vs torch.cat-based variant gradient:                       0.00e+00

This is also covered by:

  • test_gradient_correctness (already in the PR): forward 9.5e-7, backward 7.5e-8 vs the standard F.log_softmax + gather reference.
  • test_gradient_flows_through_slice_assignment (just added): explicit regression guard asserting out.is_leaf == False, out.grad_fn == CopySlices, and that logits.grad is finite and non-zero.

Why I prefer torch.empty + slice over torch.cat:

  • torch.empty + slice: peak memory = 1ร— output (chunks freed as they're written).
  • torch.cat(chunks, dim=0): peak memory = 2ร— output (all chunks kept alive until concat).

Since this PR exists specifically to reduce memory at long context, the lower-peak version is preferred.

The if N == 0 edge case is a great suggestion โ€” accepted, just pushed. Also added the explicit gradient-flow regression test, so future readers don't need to rediscover this.

Updated commit: 072cf9b (force-pushed). 16/16 unit tests pass.

@wuxibin89 wuxibin89 changed the title feat(distillation): chunked gather-logsumexp for top-K loss to avoid OOM at long context [fsdp] feat: chunked gather-logsumexp for top-K loss to avoid OOM at long context Jun 5, 2026
@wuxibin89
wuxibin89 requested a review from Luosuu June 5, 2026 03:07
Default to F.log_softmax + gather (pre-PR baseline) to preserve short-context
performance, since the chunked path has ~6x time overhead at short context
(N=14K, V=152K, bf16). Long-context users opt in via
distillation.distillation_loss.use_chunked_topk=True.

This addresses reviewer concerns about short-context regression while keeping
the OOM relief for long-context users that motivated the PR.

Trade-offs measured on H20 (V=152064, K=64, fwd+bwd):
  - short (N=14K):  baseline 21.8 GB / 16.3 ms;  chunked 30.5 GB / 94.8 ms
  - mid   (N=64K):  baseline OOM;                chunked 79.8 GB / 556.9 ms
  - long  (N=96K):  both OOM at upstream student_logits allocation (tier-1)

Adds:
  - distillation.distillation_loss.use_chunked_topk: bool = False
  - distillation.distillation_loss.chunked_topk_chunk_size: int = 4096

Existing _chunked_topk_log_probs implementation and 16 unit tests unchanged
(all still pass).
@kekellllll

kekellllll commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Update: chunked path is now opt-in (commit 73dd434)

Following the discussion above re: the tier-2 / tier-1 split, I've made the chunked path opt-in via a config flag, defaulting to False. The default behavior is now identical to the pre-PR F.log_softmax + gather, so short-context users see zero regression. Long-context users opt in via:

distillation:
  distillation_loss:
    use_chunked_topk: true
    chunked_topk_chunk_size: 4096   # tunable; default 4096

Why opt-in

Benchmarked the chunked vs baseline paths on H20 (V=152064, K=64, bf16, fwd+bwd):

context baseline chunked
short (N=14K) 21.8 GB / 16.3 ms 30.5 GB / 94.8 ms (+40% peak, +482% time)
mid (N=64K) OOM 79.8 GB / 556 ms (only path that runs)
long (N=96K) OOM (tier-1) OOM (tier-1)

A note on the +482% short-context time figure -- it's the function in
isolation. In a real training step the function accounts for only a small
fraction:

In our production OPD setup (Qwen-7B, FSDP-8, real prompt length 64K):

  • Total step time: ~225s
  • update_actor: ~108s (48%)
    • transformer fwd+bwd: ~80-90s
    • lm_head + log_softmax + gather: ~5-15s <- where this function sits
  • rollout (vLLM gen): ~50s
  • update_weights / others: ~67s

So _chunked_topk_log_probs is ~2-5% of total step time. With the +482%
local slowdown, the worst-case step time impact is +1-3%, and only when
use_chunked_topk=True is explicitly enabled (which a user would only
do if they're hitting OOM at long context, where the alternative is the
training failing entirely).

In the current agent scenario, long trajectory situations often exceed 64k, which is very necessary, and it only increases the total training time by 1% to 3%.

For default users (use_chunked_topk=False), there is zero impact.

The 6x time overhead at short context is too high to make the chunked path the default, but the OOM relief at mid context (~64K) is exactly what some users need. Opt-in solves both: short-context users keep their current performance; long-context users get a path that runs.

What I added

  • DistillationLossConfig.use_chunked_topk: bool = False
  • DistillationLossConfig.chunked_topk_chunk_size: int = 4096
  • Branch in compute_forward_kl_topk: default โ†’ F.log_softmax + gather (pre-PR baseline); opt-in โ†’ _chunked_topk_log_probs (this PR's helper, unchanged from previous commits).

What I didn't change

  • _chunked_topk_log_probs itself: unchanged, all 16 existing unit tests still pass.
  • Pre-existing baseline behaviour: bit-identical to before this PR (verified: same exact ops, same dtype path).
  • PR description: updated with the full benchmark table and the opt-in semantics; please re-skim the "Benchmark" section.

Re: tier-1 (student_logits materialisation)

Still out of scope for this PR -- I'll open a follow-up that skips the LM head in prepare_model_outputs and chunks over hidden_states directly, so neither [B, T, V] peak materialises. That's the path needed for the long (>=96K) tier where both implementations here still OOM.

@kekellllll
kekellllll requested a review from Luosuu June 6, 2026 19:18
Address review comments on commenting verbosity:
- Drop the autograd defense paragraph in _chunked_topk_log_probs docstring
  (the meta-discussion is no longer needed; behaviour is verified by
  tests and self-evident from the code).
- Slim the 22-line dispatch comment block in compute_forward_kl_topk to
  5 lines; benchmark numbers and tier-1/tier-2 scope live in the PR
  description and DistillationLossConfig docstring.
- Drop the 'Baseline path -- unchanged from pre-PR' dev comment.

No behaviour change; 16 unit tests still pass.
@kekellllll
kekellllll requested a review from Luosuu June 7, 2026 05:57
@kekellllll

kekellllll commented Jun 9, 2026 via email

Copy link
Copy Markdown
Contributor Author

Luosuu
Luosuu previously approved these changes Jun 9, 2026
@Luosuu

Luosuu commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

@kekellllll please fix pre-commit.

- Sort imports in verl/trainer/distillation/fsdp/losses.py (I001).
- Reformat assert message line in test_chunked_topk_log_probs_on_cpu.py.
- Cosmetic whitespace fixes (single-space inline comments, blank line)
  per ruff format.

No behavior change; 16 unit tests still pass.
@kekellllll

Copy link
Copy Markdown
Contributor Author

Done in e088d91 โ€” applied ruff check --fix + ruff format to the two changed files (5 insertions, 7 deletions, all whitespace / import-sort / assert-line cosmetics, no behavior change). 16 unit tests still pass.

@kekellllll
kekellllll requested a review from Luosuu June 9, 2026 06:53
@kekellllll

kekellllll commented Jun 9, 2026 via email

Copy link
Copy Markdown
Contributor Author

@Luosuu
Luosuu merged commit c6144ec into verl-project:main Jun 10, 2026
67 of 122 checks passed
wuxibin89 pushed a commit that referenced this pull request Jun 12, 2026
## Summary

- Preserve backward compatibility for lightweight distillation loss
configs that do not define `use_chunked_topk`.
- Default missing `chunked_topk_chunk_size` to the existing 4096 value
when the chunked path is enabled.

## Root Cause

PR #6593 added optional chunked top-k configuration fields, but
`compute_forward_kl_topk` accessed them directly. Existing CPU tests use
a `SimpleNamespace` distillation config with only `log_prob_min_clamp`,
so the default path raised `AttributeError` before it could fall back to
the non-chunked implementation.

## Validation

- `/root/modelchef/.venv/bin/pytest
tests/workers/test_distillation_topk_symmetry_on_cpu.py -q`
- `/root/modelchef/.venv/bin/pytest
tests/workers/test_chunked_topk_log_probs_on_cpu.py -q`
- `/root/modelchef/.venv/bin/ruff check
verl/trainer/distillation/fsdp/losses.py`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants