Skip to content

fix(specdec): correct resume and bound staging in the vLLM hidden-state dump - #2080

Open
yeyu-nvidia wants to merge 2 commits into
mainfrom
yeyu/dflash-dump-resume-and-staging
Open

fix(specdec): correct resume and bound staging in the vLLM hidden-state dump#2080
yeyu-nvidia wants to merge 2 commits into
mainfrom
yeyu/dflash-dump-resume-and-staging

Conversation

@yeyu-nvidia

@yeyu-nvidia yeyu-nvidia commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix

Fixes two issues in the vLLM offline hidden-state dump
(examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py).
Both are invisible on small dumps and only bite at scale, which is why they survived until
now — they were found while dumping ~194k conversations for a MiniMax-M3 draft.

1. Resume silently re-processed already-finished work.

keep_conversation skips conversations whose .pt already exists, but that predicate reads
on-disk state, which is not part of the fingerprint datasets computes for filter()
(it hashes the function and the dataset). With a persistent HF cache reused across a resumed
or requeued run, the cached "keep everything" result from an earlier run — computed when
few or no .pt files existed — is replayed. The run then re-generates and overwrites
conversations it had already completed, and reports Removed 0 conversations due to existing output files while doing so.

Observed on a 194k-conversation dump: ~62k .pt rewritten over a two-hour window with the
total output count completely flat.

Fix: pass load_from_cache_file=False so the filter re-checks the disk on every run.

2. Staging exhausted /dev/shm partway through large dumps.

The script generated the entire dataset before saving anything. The KV connector stages
each conversation's hidden states under its shared_storage_path (/dev/shm, i.e. RAM, by
default) and they are only freed by cleanup_hidden_states() in the save loop — so every
conversation stayed staged simultaneously. On a large dump this exhausts the space and the
connector starts failing writes:

Hidden-states write failed for req_id=...:
  SafetensorError('Error while serializing: I/O error: No space left on device (os error 28)')

Fix: generate and save in chunks of --save-chunk-size (default 256), so at most one chunk
is staged at a time. As a side benefit the dump becomes incrementally durable — an
interrupted run (walltime limit, node failure) keeps its finished conversations and the
resume path above continues from them, instead of losing the whole run's work.

Testing

  • Reproduced both failures on a 194k-conversation MiniMax-M3 dump (8-way DP, TP8), and
    confirmed both fixes on the same workload: after the change the output count advanced
    monotonically across requeues (123k → 194k) with no rewrites, and /dev/shm stayed bounded
    through completion.
  • pre-commit run --files ... passes (ruff check/format, mypy, bandit, license, rst checks).
  • Behavior is unchanged for a fresh single-shot dump other than the chunked generate calls;
    the default --save-chunk-size 256 is the only new knob.

Additional Information

Extracted from #1749, which is otherwise superseded by the streaming DFlash/DSpark path — these
two fixes are model-agnostic and apply to any offline dump, so they are worth landing on their
own.

Before your PR is "Ready for review"

  • Make sure you read and follow Contributor guidelines and your commits are signed.
  • Is this change backward compatible?: Yes
  • Did you write any new necessary tests?: No — the failure modes are multi-process/at-scale (datasets cache reuse across runs, connector RAM staging) and are not reproducible in the unit-test harness.
  • Did you add or update any necessary documentation?: Yes — CHANGELOG entry.
  • Did you update Changelog?: Yes

Summary by CodeRabbit

  • New Features

    • Added chunked hidden-state generation for large vLLM offline runs.
    • Added a configurable save-chunk size, defaulting to 256 conversations.
    • Enabled incremental saving and resumption of hidden-state outputs.
  • Bug Fixes

    • Improved resume filtering to accurately detect existing output files and avoid stale cache results.
    • Reduced memory usage by saving and releasing each generated chunk before continuing.
    • Ensured temporary files are cleaned up after interrupted or skipped saves.
    • Added validation to prevent invalid conversation IDs from creating unsafe output paths.

…te dump

Two issues in compute_hidden_states_vllm.py that only surface on large offline dumps.

Resume re-processed finished work: keep_conversation skips conversations whose .pt already
exists, but that depends on on-disk state, which is not part of the fingerprint datasets
computes from the function and the dataset. With a persistent HF cache reused across a resumed
or requeued run, the cached 'keep everything' result from an earlier run (when fewer or no .pt
existed) was replayed, so the dump re-generated and overwrote conversations it had already
finished. Observed on a 194k-conversation dump: tens of thousands of .pt rewritten over a
multi-hour window with the output count flat. Pass load_from_cache_file=False so the filter
re-checks the disk each run.

Staging exhausted /dev/shm: the whole dataset was generated before anything was saved, so every
conversation's hidden states stayed staged in the connector's shared_storage_path (/dev/shm,
i.e. RAM, by default) until the single save loop freed them. Large dumps ran out of space
partway through ('No space left on device' from the connector's safetensors write). Generate
and save in chunks of --save-chunk-size (default 256) so at most one chunk is staged. This also
makes the dump incrementally durable: an interrupted run keeps its finished conversations and
resumes from them.

Signed-off-by: Ye Yu <yeyu@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change validates conversation IDs, disables cached resume filtering, and processes hidden-state dumps in configurable chunks. The changelog records the new filtering, chunking, and cleanup behavior.

Changes

vLLM hidden-state collection flow

Layer / File(s) Summary
Resume filtering and chunk configuration
examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py
The script adds conversation-ID validation and a positive chunk-size validator. It also adds --save-chunk-size with default 256 and disables dataset cache reuse during resume filtering.
Chunked generation and cleanup
examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py, CHANGELOG.rst
The processing loop generates hidden states in chunks, saves each conversation result, updates progress, and frees staged data in finally blocks. The changelog records the same behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: chenhanyu, aanoosheh

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes to resume behavior and staging usage in the vLLM hidden-state dump.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Security Anti-Patterns ✅ Passed Code review against SECURITY.md practices: (1) No torch.load() calls with weights_only=False; (2) No numpy.load() with allow_pickle=True; (3) trust_remote_code is a caller-configurable CLI flag (--...
✨ 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 yeyu/dflash-dump-resume-and-staging

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

@coderabbitai coderabbitai 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 3

🤖 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
`@examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py`:
- Around line 113-121: Validate --save-chunk-size at argument parsing by
requiring a strictly positive integer, causing argparse to reject zero and
negative values. In the processing logic around the save-chunk-size usage,
remove the silent conversion to 1 and use args.save_chunk_size directly.
- Around line 318-354: Wrap all processing after hidden_states_path is acquired,
including validation and file saving, in a try block, and move
cleanup_hidden_states(hidden_states_path) to a finally block so it runs on every
path, including the short loss_mask continue. Use the existing
hidden_states_path and example_hidden_states_connector symbols without changing
the processing behavior.
- Around line 342-343: Validate and constrain conversation_id at the dataset
boundary before constructing output_file, including the resume check and write
around output_file. Reject absolute paths and traversal components, allowing
only safe identifiers that remain within output_dir, and preserve normal
processing for valid IDs.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 4c2e0717-1d85-4d7d-bcd8-a7c8c733f9f0

📥 Commits

Reviewing files that changed from the base of the PR and between 7afbfbc and c4a56fa.

📒 Files selected for processing (2)
  • CHANGELOG.rst
  • examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py

Comment thread examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py Outdated
Comment thread examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py Outdated
…chunk size

- Free each conversation's staged hidden states in a finally, so a conversation skipped
  mid-loop (short loss_mask) no longer leaks its staging file. This is the same failure this
  PR set out to prevent, on an error path.
- Validate conversation_id as a plain filename where the dataset is read: it is used directly
  as the output filename, so an absolute path or one containing a separator or '..' would
  resolve outside --output-dir.
- Reject non-positive --save-chunk-size at argument parsing instead of silently clamping.

Signed-off-by: Ye Yu <yeyu@nvidia.com>

@coderabbitai coderabbitai 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 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
`@examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py`:
- Around line 371-382: Update the save path in compute_hidden_states_vllm so
each conversation dump is written atomically: write the torch.save payload to a
temporary file in output_dir first, then replace the final output_file only
after serialization succeeds. Keep the existing output_file naming and payload
structure in the same save block, and anchor the change around the
open(output_file, "wb") / torch.save call site so the resume filter never sees a
partially written .pt file.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2adf9037-fb2d-4ed4-bfc0-0a960c9357fa

📥 Commits

Reviewing files that changed from the base of the PR and between c4a56fa and abbfb95.

📒 Files selected for processing (2)
  • CHANGELOG.rst
  • examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.rst

Comment on lines +371 to +382
output_file = output_dir / f"{conv_id}.pt"
with open(output_file, "wb") as f:
torch.save(
{
"input_ids": token_ids.cpu(),
"hidden_states": output_hidden_states,
"aux_hidden_states": aux_hidden_states,
"loss_mask": loss_mask[: output_hidden_states.shape[0]].cpu(),
"conversation_id": conv_id,
},
f,
)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Write each completed dump atomically.

The resume filter at Line 193 treats any existing .pt file as complete. torch.save() writes directly to that final path. If serialization fails or the process stops during the write, a truncated file remains. A resumed run then skips that conversation permanently.

Write to a temporary file in output_dir. Replace the final path only after torch.save() succeeds.

Proposed fix
                 output_file = output_dir / f"{conv_id}.pt"
-                with open(output_file, "wb") as f:
+                temp_output_file = output_file.with_suffix(f"{output_file.suffix}.tmp")
+                with open(temp_output_file, "wb") as f:
                     torch.save(
                         {
                             "input_ids": token_ids.cpu(),
                             "hidden_states": output_hidden_states,
                             "aux_hidden_states": aux_hidden_states,
                             "loss_mask": loss_mask[: output_hidden_states.shape[0]].cpu(),
                             "conversation_id": conv_id,
                         },
                         f,
                     )
+                os.replace(temp_output_file, output_file)
📝 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
output_file = output_dir / f"{conv_id}.pt"
with open(output_file, "wb") as f:
torch.save(
{
"input_ids": token_ids.cpu(),
"hidden_states": output_hidden_states,
"aux_hidden_states": aux_hidden_states,
"loss_mask": loss_mask[: output_hidden_states.shape[0]].cpu(),
"conversation_id": conv_id,
},
f,
)
output_file = output_dir / f"{conv_id}.pt"
temp_output_file = output_file.with_suffix(f"{output_file.suffix}.tmp")
with open(temp_output_file, "wb") as f:
torch.save(
{
"input_ids": token_ids.cpu(),
"hidden_states": output_hidden_states,
"aux_hidden_states": aux_hidden_states,
"loss_mask": loss_mask[: output_hidden_states.shape[0]].cpu(),
"conversation_id": conv_id,
},
f,
)
os.replace(temp_output_file, output_file)
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 371-371: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(output_file, "wb")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 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
`@examples/speculative_decoding/collect_hidden_states/compute_hidden_states_vllm.py`
around lines 371 - 382, Update the save path in compute_hidden_states_vllm so
each conversation dump is written atomically: write the torch.save payload to a
temporary file in output_dir first, then replace the final output_file only
after serialization succeeds. Keep the existing output_file naming and payload
structure in the same save block, and anchor the change around the
open(output_file, "wb") / torch.save call site so the resume filter never sees a
partially written .pt file.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.02%. Comparing base (7afbfbc) to head (abbfb95).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2080      +/-   ##
==========================================
- Coverage   67.17%   67.02%   -0.15%     
==========================================
  Files         521      521              
  Lines       59857    59857              
==========================================
- Hits        40206    40117      -89     
- Misses      19651    19740      +89     
Flag Coverage Δ
examples 43.04% <ø> (-0.21%) ⬇️
regression 14.96% <ø> (+0.07%) ⬆️
unit 55.39% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

1 participant