Skip to content

Expose raw logits for GLM KLD capture - #32

Closed
voipmonitor wants to merge 2 commits into
codex/dark-devotion-dcp4-mtp3-globaltopk-fix-20260621from
codex/dark-devotion-kld-logits-export-20260621
Closed

Expose raw logits for GLM KLD capture#32
voipmonitor wants to merge 2 commits into
codex/dark-devotion-dcp4-mtp3-globaltopk-fix-20260621from
codex/dark-devotion-kld-logits-export-20260621

Conversation

@voipmonitor

@voipmonitor voipmonitor commented Jun 21, 2026

Copy link
Copy Markdown

Summary

  • Add optional SamplingParams.return_prompt_logits and return_sample_logits debug flags.
  • Carry raw prompt/decode logits through the v1 scheduler, worker, sampler, and request output path.
  • Keep the default behavior unchanged; logits are only materialized when explicitly requested by the KLD capture scripts.

Why

GLM-5.2 KLD comparisons currently require copying a local vLLM overlay into the container before running prefill/decode captures. This PR makes that capture path reproducible from a branch/image instead of a one-off overlay.

Validation

  • python3 -m py_compile on all 16 modified Python files.

Stack

Stacked on PR #31 (codex/dark-devotion-dcp4-mtp3-globaltopk-fix-20260621) so this diff only contains the raw-logits capture plumbing.

Summary by CodeRabbit

  • New Features
    • Added return_prompt_logits and return_sample_logits sampling options to retrieve raw logits tensors for offline numerical comparison and analysis.

Keep the B12X DCP global top-k path rank-consistent through graph warmup and MTP draft execution.

The target GLM path already allocated score buffers for B12X DCP top-k, but the MTP draft layer only allocated indices. With B12X sparse indexer global-topk enabled, the draft capture either failed or fell back to non-comparable slow paths. Allocate and pass top-k score buffers for MTP when DCP and B12X sparse indexer are active.

Also keep empty DCP prefill chunks participating in the global-topk merge and prewarm the actual B12X merge/extend paths so rank-sensitive graph warmup enters the same collectives on every rank.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds two new SamplingParams flags—return_prompt_logits and return_sample_logits—that propagate raw logit tensors through the entire v1 engine pipeline: from GPU sampler and prompt-logprob worker, through async CPU transfer, model runner output, scheduler per-request slicing, and engine-side accumulation into CompletionOutput.sample_logits and RequestOutput.prompt_logits.

Changes

Raw Logits Output Pipeline

Layer / File(s) Summary
Public API and output schema contracts
vllm/sampling_params.py, vllm/outputs.py, vllm/v1/outputs.py, vllm/v1/engine/__init__.py, vllm/v1/worker/gpu/sample/output.py
SamplingParams gains return_prompt_logits and return_sample_logits booleans; __post_init__ auto-sets prompt_logprobs=1 and broadens prefix-cache skip when return_prompt_logits is enabled. CompletionOutput.sample_logits, RequestOutput.prompt_logits, ModelRunnerOutput.{prompt_logits_dict,sample_logits}, SamplerOutput.sample_logits (nullable), and EngineCoreOutput.{new_prompt_logits,new_sample_logits} fields are added. RequestOutput.add merges sample_logits via torch.cat.
Sample logits: per-request state tracking and sampler
vllm/v1/worker/gpu/sample/states.py, vllm/v1/worker/gpu/sample/sampler.py, vllm/v1/worker/gpu/spec_decode/rejection_sampler.py
SamplingStates adds a return_sample_logits boolean array populated in add_request and queried by the new any_return_sample_logits predicate. Sampler.__call__ conditionally sets SamplerOutput.sample_logits = logits.detach(). RejectionSampler passes explicit sample_logits=None.
Prompt logits: PromptLogprobsWorker and _get_prompt_logprobs_dict
vllm/v1/worker/gpu_input_batch.py, vllm/v1/worker/gpu/sample/prompt_logprob.py, vllm/v1/worker/gpu_model_runner.py
CachedRequestState adds in_progress_prompt_logits chunk buffer. PromptLogprobsWorker tracks per-request return_prompt_logits, initializes/clears logit buffers, and passes the flag into compute_prompt_logprobs_with_chunking, which gains a return_prompt_logits param and returns an additional concatenated CPU float tensor (or None). GPUModelRunner._get_prompt_logprobs_dict changes return type to a two-dict tuple, adds a branch for GPU→CPU logit collection across chunked prefill, and clears buffers on completion.
Model runner wiring and async GPU→CPU transfer
vllm/v1/worker/gpu/model_runner.py, vllm/v1/worker/gpu_model_runner.py, vllm/v1/worker/gpu/async_utils.py
sample_tokens / _bookkeeping_sync unpack prompt_logits_dict as a second return value and include it in ModelRunnerOutput. AsyncOutput.__init__ non-blockingly copies sample_logits to CPU and snapshots prompt_logits_dict; get_output restores both onto model_runner_output after stream sync.
Scheduler propagation and engine-side output assembly
vllm/v1/core/sched/scheduler.py, vllm/v1/engine/logprobs.py, vllm/v1/engine/output_processor.py
Scheduler.update_from_output extracts and slices per-request prompt_logits and sample_logits into EngineCoreOutput. LogprobsProcessor gains prompt_logits and sample_logits fields accumulated in update_from_output. OutputProcessor threads these into RequestOutput.prompt_logits and CompletionOutput.sample_logits.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Expose raw logits for GLM KLD capture' clearly summarizes the primary change: adding functionality to expose/capture raw logits for GLM KLD workflows, which aligns with the PR objectives and the modifications across 16 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/dark-devotion-kld-logits-export-20260621

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
vllm/v1/worker/gpu_model_runner.py (3)

3658-3666: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update _bookkeeping_sync’s return annotation.

The function now returns prompt_logits_dict as an additional tuple element, but the annotation still describes the old 7-element shape.

Proposed fix
     ) -> tuple[
         dict[str, int],
         LogprobsLists | None,
         list[list[int]],
         dict[str, LogprobsTensors | None],
+        dict[str, torch.Tensor | None],
         list[str],
         dict[str, int],
         list[int],
     ]:

Also applies to: 3782-3790

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vllm/v1/worker/gpu_model_runner.py` around lines 3658 - 3666, The return type
annotation for the `_bookkeeping_sync` method is outdated and does not reflect
the actual return tuple structure. The method now returns an additional
`prompt_logits_dict` element but the tuple annotation at lines 3658-3666 still
shows the old 7-element shape. Add the missing `dict[str, int]` type annotation
to the tuple to account for the `prompt_logits_dict` return value. Also update
the matching return annotation at lines 3782-3790 to ensure consistency.

4674-4680: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Thread sampler_output.sample_logits into this runner output.

prompt_logits_dict is now forwarded, but sample logits are not attached to ModelRunnerOutput or copied by this file’s async wrapper, so return_sample_logits can be silently dropped on this path.

Mirror the sample-logit CPU transfer/output handling used by the updated async output path for both sync and async scheduling.

Also applies to: 4730-4738

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vllm/v1/worker/gpu_model_runner.py` around lines 4674 - 4680, The
ModelRunnerOutput being constructed is missing the sample logits from
sampler_output.sample_logits, even though prompt_logits_dict is being forwarded.
Add the sample_logits field to the ModelRunnerOutput construction at the
location where prompt_logits_dict is being set, ensuring it captures
sampler_output.sample_logits. Additionally, ensure that the CPU transfer and
output handling for sample logits mirrors the approach used in the async output
path (referenced at lines 4730-4738) so that sample logits are properly handled
consistently across both sync and async scheduling paths and the
return_sample_logits flag is not silently dropped.

5588-5612: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Flush buffered logits and don’t suppress requested prompt logprobs.

The new continue makes return_prompt_logits skip prompt_logprobs output when both are requested, and the num_logits <= 0 completion path can clear already-buffered prompt logits without returning them.

Proposed fix
             if num_logits <= 0:
                 # This can happen for the final chunk if we prefilled exactly
                 # (num_prompt_tokens - 1) tokens for this request in the prior
                 # step. There are no more prompt logprobs to produce.
+                if req_id in completed_prefill_reqs:
+                    if (
+                        return_prompt_logits
+                        and request.in_progress_prompt_logits is not None
+                    ):
+                        prompt_logits_dict[req_id] = torch.cat(
+                            request.in_progress_prompt_logits, dim=0
+                        )
+                    if request.in_progress_prompt_logprobs_cpu is not None:
+                        prompt_logprobs_dict[req_id] = (
+                            request.in_progress_prompt_logprobs_cpu
+                        )
                 continue
@@
                 if req_id in completed_prefill_reqs:
                     prompt_logits_dict[req_id] = torch.cat(
                         request.in_progress_prompt_logits, dim=0
                     )
                     request.in_progress_prompt_logits = None
-                continue
+                if (
+                    request.sampling_params is None
+                    or request.sampling_params.prompt_logprobs is None
+                ):
+                    continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vllm/v1/worker/gpu_model_runner.py` around lines 5588 - 5612, The code has
two issues: first, the `continue` statement inside the `return_prompt_logits`
block prevents subsequent prompt_logprobs processing from running when both
features are requested, and second, the `num_logits <= 0` early continue path
exits without flushing any buffered logits that may have been accumulated in
`request.in_progress_prompt_logits`. To fix this, remove the `continue`
statement in the `return_prompt_logits` block to allow prompt_logprobs
processing to proceed, and in the `num_logits <= 0` condition, before
continuing, check if `request.in_progress_prompt_logits` has buffered data and
flush it to `prompt_logits_dict` with the accumulated logits (using torch.cat
like in the completed prefill case).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vllm/v1/engine/output_processor.py`:
- Line 371: In DELTA mode (RequestOutputKind.DELTA), the output should only
include logits for newly generated tokens, but the code at line 371 is including
self.logprobs_processor.prompt_logits which represents cumulative/non-delta
data. Remove the prompt_logits field when RequestOutputKind is DELTA and ensure
only delta sample_logits (logits for newly sampled tokens) are emitted. Apply
this same fix at the second location mentioned at line 409 where the same
pattern appears.

In `@vllm/v1/worker/gpu/sample/prompt_logprob.py`:
- Line 95: The issue is that line 95 uses np.any(returns_prompt_logits) to
convert a per-request boolean array into a single batch-wide flag, which causes
the helper to materialize raw prompt logits for all requests in the batch even
when only some requests need them. Then lines 253-255 copy all prompt-logit
chunks to CPU unnecessarily. To fix this, either pass the per-request
returns_prompt_logits array directly as a mask instead of aggregating it with
np.any(), or move the CPU copy logic at lines 253-255 to only execute for
requests where the corresponding element in returns_prompt_logits is True,
ensuring raw logits are materialized only for flagged requests.
- Around line 145-146: The condition in the if statement that checks
logits_chunk.numel() and the compound boolean expression (req_is_prompt_chunked
or prompt_logits_list) exceeds the 88 character line length limit. Break this
condition across multiple lines by wrapping it after the logical operators,
ensuring each line stays within the character limit while maintaining code
readability. Use parentheses to group related conditions together if needed.

---

Outside diff comments:
In `@vllm/v1/worker/gpu_model_runner.py`:
- Around line 3658-3666: The return type annotation for the `_bookkeeping_sync`
method is outdated and does not reflect the actual return tuple structure. The
method now returns an additional `prompt_logits_dict` element but the tuple
annotation at lines 3658-3666 still shows the old 7-element shape. Add the
missing `dict[str, int]` type annotation to the tuple to account for the
`prompt_logits_dict` return value. Also update the matching return annotation at
lines 3782-3790 to ensure consistency.
- Around line 4674-4680: The ModelRunnerOutput being constructed is missing the
sample logits from sampler_output.sample_logits, even though prompt_logits_dict
is being forwarded. Add the sample_logits field to the ModelRunnerOutput
construction at the location where prompt_logits_dict is being set, ensuring it
captures sampler_output.sample_logits. Additionally, ensure that the CPU
transfer and output handling for sample logits mirrors the approach used in the
async output path (referenced at lines 4730-4738) so that sample logits are
properly handled consistently across both sync and async scheduling paths and
the return_sample_logits flag is not silently dropped.
- Around line 5588-5612: The code has two issues: first, the `continue`
statement inside the `return_prompt_logits` block prevents subsequent
prompt_logprobs processing from running when both features are requested, and
second, the `num_logits <= 0` early continue path exits without flushing any
buffered logits that may have been accumulated in
`request.in_progress_prompt_logits`. To fix this, remove the `continue`
statement in the `return_prompt_logits` block to allow prompt_logprobs
processing to proceed, and in the `num_logits <= 0` condition, before
continuing, check if `request.in_progress_prompt_logits` has buffered data and
flush it to `prompt_logits_dict` with the accumulated logits (using torch.cat
like in the completed prefill case).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec963c98-238d-495c-afb4-9c1902cd7754

📥 Commits

Reviewing files that changed from the base of the PR and between 000807e and 199d43e.

📒 Files selected for processing (16)
  • vllm/outputs.py
  • vllm/sampling_params.py
  • vllm/v1/core/sched/scheduler.py
  • vllm/v1/engine/__init__.py
  • vllm/v1/engine/logprobs.py
  • vllm/v1/engine/output_processor.py
  • vllm/v1/outputs.py
  • vllm/v1/worker/gpu/async_utils.py
  • vllm/v1/worker/gpu/model_runner.py
  • vllm/v1/worker/gpu/sample/output.py
  • vllm/v1/worker/gpu/sample/prompt_logprob.py
  • vllm/v1/worker/gpu/sample/sampler.py
  • vllm/v1/worker/gpu/sample/states.py
  • vllm/v1/worker/gpu/spec_decode/rejection_sampler.py
  • vllm/v1/worker/gpu_input_batch.py
  • vllm/v1/worker/gpu_model_runner.py

prompt_logprobs=prompt_logprobs,
outputs=cast(list[CompletionOutput], outputs),
finished=finished,
prompt_logits=self.logprobs_processor.prompt_logits,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

DELTA mode is emitting non-delta logits payloads.

RequestOutputKind.DELTA sends delta token_ids, but this path currently re-emits
stored prompt_logits and cumulative sample_logits. That breaks delta output
shape/semantics and can duplicate logits during downstream merge.

Proposed fix
@@
-        if self.output_kind == RequestOutputKind.DELTA:
+        prompt_logits = self.logprobs_processor.prompt_logits
+        if self.output_kind == RequestOutputKind.DELTA:
             # Side effect: logprobs processor forgets prompt logprobs
             prompt_logprobs = self.logprobs_processor.pop_prompt_logprobs()
+            # Emit prompt logits once in DELTA mode.
+            self.logprobs_processor.prompt_logits = None
         else:
             prompt_logprobs = self.logprobs_processor.prompt_logprobs
@@
-            prompt_logits=self.logprobs_processor.prompt_logits,
+            prompt_logits=prompt_logits,
@@
-        return CompletionOutput(
+        sample_logits = self.logprobs_processor.sample_logits
+        if delta:
+            sample_logits = (
+                sample_logits[-len(token_ids):]
+                if sample_logits is not None and token_ids
+                else None
+            )
+
+        return CompletionOutput(
@@
-            sample_logits=self.logprobs_processor.sample_logits,
+            sample_logits=sample_logits,

Also applies to: 409-409

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vllm/v1/engine/output_processor.py` at line 371, In DELTA mode
(RequestOutputKind.DELTA), the output should only include logits for newly
generated tokens, but the code at line 371 is including
self.logprobs_processor.prompt_logits which represents cumulative/non-delta
data. Remove the prompt_logits field when RequestOutputKind is DELTA and ensure
only delta sample_logits (logits for newly sampled tokens) are emitted. Apply
this same fix at the second location mentioned at line 409 where the same
pattern appears.

hidden_states[: input_batch.num_tokens],
logits_fn,
max_num_prompt_logprobs,
return_prompt_logits=np.any(returns_prompt_logits),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Only materialize raw prompt logits for flagged requests.

Line 95 turns on helper-level logit capture for the whole batch when any request asks for prompt logits, and Lines 253-255 then copy every prompt-logit chunk to CPU. In mixed batches, one debug request can force full-vocab CPU copies for unrelated prompt-logprob requests. Pass a per-token/request mask or move the CPU copy to flagged request slices so raw logits are materialized only for return_prompt_logits requests.

Also applies to: 253-255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vllm/v1/worker/gpu/sample/prompt_logprob.py` at line 95, The issue is that
line 95 uses np.any(returns_prompt_logits) to convert a per-request boolean
array into a single batch-wide flag, which causes the helper to materialize raw
prompt logits for all requests in the batch even when only some requests need
them. Then lines 253-255 copy all prompt-logit chunks to CPU unnecessarily. To
fix this, either pass the per-request returns_prompt_logits array directly as a
mask instead of aggregating it with np.any(), or move the CPU copy logic at
lines 253-255 to only execute for requests where the corresponding element in
returns_prompt_logits is True, ensuring raw logits are materialized only for
flagged requests.

Comment on lines +145 to +146
if logits_chunk.numel() and (req_is_prompt_chunked or prompt_logits_list):
prompt_logits_list.append(logits_chunk)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap this condition to stay within the Python line limit.

Suggested formatting fix
-                if logits_chunk.numel() and (req_is_prompt_chunked or prompt_logits_list):
+                if (
+                    logits_chunk.numel()
+                    and (req_is_prompt_chunked or prompt_logits_list)
+                ):
                     prompt_logits_list.append(logits_chunk)

As per coding guidelines, Maintain a line length limit of 88 characters for Python code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if logits_chunk.numel() and (req_is_prompt_chunked or prompt_logits_list):
prompt_logits_list.append(logits_chunk)
if (
logits_chunk.numel()
and (req_is_prompt_chunked or prompt_logits_list)
):
prompt_logits_list.append(logits_chunk)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vllm/v1/worker/gpu/sample/prompt_logprob.py` around lines 145 - 146, The
condition in the if statement that checks logits_chunk.numel() and the compound
boolean expression (req_is_prompt_chunked or prompt_logits_list) exceeds the 88
character line length limit. Break this condition across multiple lines by
wrapping it after the logical operators, ensuring each line stays within the
character limit while maintaining code readability. Use parentheses to group
related conditions together if needed.

Source: Coding guidelines

@voipmonitor
voipmonitor force-pushed the codex/dark-devotion-dcp4-mtp3-globaltopk-fix-20260621 branch from 000807e to 79f154c Compare June 21, 2026 15:29
@lukealonso lukealonso closed this Jun 28, 2026
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