Skip to content
Merged
Show file tree
Hide file tree
Changes from 130 commits
Commits
Show all changes
132 commits
Select commit Hold shift + click to select a range
1a320a7
Implement Mask Replay
vx120 Jul 23, 2026
a444039
Optimize sampling mask storage
vx120 Jul 24, 2026
9c6db21
Streamlined the code and removed unnecessary parts.
vx120 Jul 27, 2026
95d453f
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 27, 2026
da36481
Require final-only output for sampling replay
vx120 Jul 29, 2026
a37a134
add pre-commit code
vx120 Jul 29, 2026
2129592
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 29, 2026
e3b8d31
Clean up formatting in scheduler.py
vx120 Jul 29, 2026
bbffe02
Require bounded top-k for sampling replay
vx120 Jul 30, 2026
fee020e
Improve error message for top_k validation
vx120 Jul 30, 2026
145da58
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 30, 2026
d84da9d
pre-commit change
vx120 Jul 30, 2026
8c4144f
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 30, 2026
045c413
Validate sampling replay output mode early
aoshen02 Jul 30, 2026
2951912
Gate sampling masks in scheduler
aoshen02 Jul 30, 2026
754a737
Merge pull request #3 from aoshen02/codex/fix-sampling-replay-validation
aoshen02 Jul 30, 2026
8cdca91
Compact sampling support before async output
aoshen02 Jul 30, 2026
4bce273
Merge pull request #9 from aoshen02/codex/compact-sampling-support-ou…
aoshen02 Jul 30, 2026
26199bf
Merge pull request #6 from aoshen02/codex/sampling-mask-scheduler-flag
aoshen02 Jul 30, 2026
fdae7cc
Remove FlashInfer sampling mask log
aoshen02 Jul 30, 2026
0acee62
Merge pull request #10 from aoshen02/codex/remove-flashinfer-sampling…
aoshen02 Jul 30, 2026
41f18c6
Disable FlashInfer sampling for sampling masks
aoshen02 Jul 30, 2026
a5591cc
Merge pull request #11 from aoshen02/codex/disable-flashinfer-samplin…
aoshen02 Jul 30, 2026
ba8aa3a
Keep sampling mask tensors local to sampler
aoshen02 Jul 30, 2026
f038732
Merge pull request #12 from aoshen02/codex/keep-sampling-mask-local
aoshen02 Jul 30, 2026
880788b
Move sampling mask to its own module
aoshen02 Jul 30, 2026
8622dbd
Merge pull request #14 from aoshen02/codex/extract-sampling-mask-output
aoshen02 Jul 30, 2026
00c2ed2
Restore sampler spacing
aoshen02 Jul 31, 2026
0ac290f
Merge pull request #16 from aoshen02/codex/restore-processed-logprobs
aoshen02 Jul 31, 2026
209a885
refactor: align sampling mask tensor conversion
aoshen02 Jul 31, 2026
cef55ab
Merge pull request #17 from aoshen02/codex/python-sampling-mask-conve…
aoshen02 Jul 31, 2026
b25bed1
Refactor sampling mask compaction
aoshen02 Jul 31, 2026
fac80d4
Merge pull request #18 from aoshen02/codex/encapsulate-sampling-mask-…
aoshen02 Jul 31, 2026
78b54ac
Merge branch 'main' into mask_replay
aoshen02 Jul 31, 2026
3c5e664
Align sampling mask output with Rust protocol
vx120 Jul 31, 2026
cf2ad7e
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 31, 2026
1c7b816
[Feature] Expose sampling mask via HTTP endpoints as list[list[int]]
aoshen02 Jul 31, 2026
a9ae6e5
Remove sampling_mask from OpenAI-compat endpoints
aoshen02 Jul 31, 2026
c3a2a38
Merge pull request #19 from aoshen02/worktree-sampling-mask-endpoint
aoshen02 Jul 31, 2026
2d947dd
Sync sampling mask protocol fixtures
vx120 Jul 31, 2026
8905ebb
Enforce processed_logprobs mode for sampling mask
aoshen02 Jul 31, 2026
1fccd75
Merge pull request #20 from aoshen02/fix-logprobs-mode-validation
aoshen02 Jul 31, 2026
1e71000
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
03fae73
Fix sampling mask scheduler test setup
vx120 Jul 31, 2026
3963b7e
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 31, 2026
d886296
Move new_sampling_mask to end of EngineCoreOutput, revert Rust changes
aoshen02 Jul 31, 2026
95f6a6e
Merge pull request #21 from aoshen02/fix-sampling-mask-field-order
aoshen02 Jul 31, 2026
ac6602f
Fix python_compat.py: move new_sampling_mask to end, remove wire Nil
aoshen02 Jul 31, 2026
97a96a0
Update output.rs
aoshen02 Jul 31, 2026
26b767b
Merge pull request #22 from aoshen02/fix-python-compat-field-order
aoshen02 Jul 31, 2026
e0d43ad
Remove stale sampling_mask wire Nil from client.rs test fixture
aoshen02 Jul 31, 2026
19c3ff9
Merge pull request #23 from aoshen02/fix-client-rs-wire-nil
aoshen02 Jul 31, 2026
f8427ac
Remove FINAL_ONLY requirement for sampling mask
aoshen02 Jul 31, 2026
bbaa49c
Merge pull request #24 from aoshen02/remove-final-only-requirement
aoshen02 Jul 31, 2026
6d3836e
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
e4e0ae1
add validation of sampling replay params
vx120 Jul 31, 2026
fb14bec
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
a74b537
Add sampling mask feature documentation
aoshen02 Jul 31, 2026
278a7b2
Merge pull request #25 from aoshen02/add-sampling-mask-docs
aoshen02 Jul 31, 2026
6e7747a
fix: markdownlint MD060 table-column-style in sampling_mask.md
aoshen02 Jul 31, 2026
695ca45
Merge pull request #26 from vx120/fix/sampling-mask-md-lint-49577
aoshen02 Jul 31, 2026
7cf69ba
Remove logprobs check in input_processor.py
vx120 Jul 31, 2026
bf2865d
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
a49d465
[Feature] Expose sampling mask via HTTP endpoints as list[list[int]]
aoshen02 Jul 31, 2026
7f90a9f
Remove sampling_mask from OpenAI-compat endpoints
aoshen02 Jul 31, 2026
4421bd4
Merge pull request #19 from aoshen02/worktree-sampling-mask-endpoint
aoshen02 Jul 31, 2026
93dc02e
Enforce processed_logprobs mode for sampling mask
aoshen02 Jul 31, 2026
ca15b4c
Sync sampling mask protocol fixtures
vx120 Jul 31, 2026
982ceb9
Merge pull request #20 from aoshen02/fix-logprobs-mode-validation
aoshen02 Jul 31, 2026
b799148
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
1733f82
Move new_sampling_mask to end of EngineCoreOutput, revert Rust changes
aoshen02 Jul 31, 2026
4dcc2cc
Fix sampling mask scheduler test setup
vx120 Jul 31, 2026
b8fcc29
Merge branch 'vllm-project:main' into mask_replay
vx120 Jul 31, 2026
e77d1d9
Merge pull request #21 from aoshen02/fix-sampling-mask-field-order
aoshen02 Jul 31, 2026
cb8c60a
Fix python_compat.py: move new_sampling_mask to end, remove wire Nil
aoshen02 Jul 31, 2026
fcd283a
Update output.rs
aoshen02 Jul 31, 2026
f332d61
Remove stale sampling_mask wire Nil from client.rs test fixture
aoshen02 Jul 31, 2026
87fd402
Merge pull request #22 from aoshen02/fix-python-compat-field-order
aoshen02 Jul 31, 2026
a69ae8c
Remove FINAL_ONLY requirement for sampling mask
aoshen02 Jul 31, 2026
ee4f6fb
Merge pull request #23 from aoshen02/fix-client-rs-wire-nil
aoshen02 Jul 31, 2026
73f7c71
Merge pull request #24 from aoshen02/remove-final-only-requirement
aoshen02 Jul 31, 2026
8987b3d
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
e2d4fa0
add validation of sampling replay params
vx120 Jul 31, 2026
55d701b
Add sampling mask feature documentation
aoshen02 Jul 31, 2026
7c9722d
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
818b022
Merge pull request #25 from aoshen02/add-sampling-mask-docs
aoshen02 Jul 31, 2026
402766a
fix: markdownlint MD060 table-column-style in sampling_mask.md
aoshen02 Jul 31, 2026
decfccb
Merge pull request #26 from vx120/fix/sampling-mask-md-lint-49577
aoshen02 Jul 31, 2026
7f10866
fix(rust): add new_sampling_mask slot to EngineCoreOutput
aoshen02 Jul 31, 2026
911496d
Remove logprobs check in input_processor.py
vx120 Jul 31, 2026
8235f6b
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
90d17ea
Merge pull request #27 from vx120/fix/sampling-mask-md-lint-49577
aoshen02 Jul 31, 2026
6dae808
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
bb08e6f
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
16e1c80
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
6d7edbd
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
79c3986
Merge branch 'main' into mask_replay
vx120 Jul 31, 2026
d8fcd2c
Merge branch 'main' into mask_replay
vx120 Aug 1, 2026
f0e800d
Merge branch 'mask_replay' of https://github.com/vx120/vllm into mask…
vx120 Aug 4, 2026
de61e3c
Merge branch 'main' into mask_replay
vx120 Aug 4, 2026
8758c7a
Merge branch 'mask_replay' of https://github.com/vx120/vllm into mask…
vx120 Aug 4, 2026
966e96c
Avoid GPU sync when collecting sampling masks
vx120 Aug 4, 2026
95cecfe
Merge branch 'main' into mask_replay
vx120 Aug 4, 2026
cd4eaf1
Merge branch 'main' into mask_replay
vx120 Aug 5, 2026
e69e436
Merge branch 'main' into mask_replay
vx120 Aug 5, 2026
ca46f77
add pre-commit code
vx120 Aug 5, 2026
19c6c86
Merge branch 'main' into mask_replay
vx120 Aug 5, 2026
997eb90
Merge branch 'main' into mask_replay
vx120 Aug 5, 2026
d49e7e4
Merge upstream main into mask_replay
vx120 Aug 10, 2026
2ec706d
Merge branch 'main' into mask_replay
vx120 Aug 10, 2026
9c793ef
Fix pre-commit import ordering
vx120 Aug 10, 2026
4d638b1
Merge remote-tracking branch 'origin/mask_replay' into mask_replay
vx120 Aug 10, 2026
e97181d
Merge branch 'main' into mask_replay
vx120 Aug 10, 2026
38fc120
Merge branch 'main' into mask_replay
aoshen02 Aug 11, 2026
572fdbd
Fix sampling mask tensor test assertions
aoshen02 Aug 11, 2026
442d302
Merge pull request #28 from aoshen02/codex/fix-sampling-mask-tests
aoshen02 Aug 11, 2026
13e9e1f
Merge branch 'main' into mask_replay
aoshen02 Aug 11, 2026
a23eeb6
Remove sampling mask CPU fallback
aoshen02 Aug 12, 2026
ffeac59
Merge pull request #29 from aoshen02/codex/remove-sampling-mask-cpu-f…
aoshen02 Aug 12, 2026
f90415f
Merge branch 'main' into mask_replay
aoshen02 Aug 12, 2026
9e08019
Merge branch 'main' into mask_replay
vx120 Aug 12, 2026
59c64b9
Address sampling mask review feedback
aoshen02 Aug 12, 2026
c992544
Add sampling mask endpoint coverage
aoshen02 Aug 12, 2026
63f97c6
Merge pull request #30 from aoshen02/codex/address-mask-replay-review…
aoshen02 Aug 12, 2026
c937874
Merge branch 'main' into mask_replay
aoshen02 Aug 12, 2026
bbe9552
Fix sampling mask support assertions
aoshen02 Aug 13, 2026
2eae885
Merge pull request #31 from aoshen02/codex/fix-sampling-mask-support-…
aoshen02 Aug 13, 2026
ab0c94b
Fix sampling mask Rust wire compatibility
aoshen02 Aug 13, 2026
0276e94
Merge pull request #32 from aoshen02/codex/fix-sampling-mask-rust-wire
aoshen02 Aug 13, 2026
b53b2a9
Merge branch 'main' into mask_replay
aoshen02 Aug 13, 2026
5b15d36
Fix Rust sampling mask test formatting
aoshen02 Aug 13, 2026
ab01a11
Merge pull request #33 from aoshen02/codex/fix-sampling-mask-rustfmt
aoshen02 Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions docs/training/sampling_mask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Sampling Mask (Distribution Replay)

When using top-k/top-p sampling for RL rollouts (e.g. GRPO), there is a
systematic mismatch between the truncated distribution the sampler actually
drew from and the full-vocabulary softmax used to compute log-probabilities
during training. The **sampling mask** feature closes this gap by returning
the exact set of token IDs that survived top-k/top-p/min-p filtering at each
generation step, so the training side can normalize over the same support.

## Background

This feature implements the **Keep Sampling Mask** strategy described in the
[DeepSeek-V3.2 technical report](https://huggingface.co/deepseek-ai/DeepSeek-V3.2/blob/main/assets/paper.pdf)
(Section 3.3). The key insight: top-k/top-p truncation during rollout sampling
introduces a mismatch between the action spaces of `π_old` and `π_θ`, which
violates the principles of importance sampling and destabilizes training. By
preserving the truncation masks from `π_old` and applying them to `π_θ` during
training, both policies share identical action subspaces. DeepSeek reports that
combining top-p sampling with the Keep Sampling Mask strategy effectively
preserves language consistency during RL training.

## Quick start

```bash
vllm serve <model> \
--return-sampling-mask \
--logprobs-mode processed_logprobs
```

```python
from vllm import LLM, SamplingParams

llm = LLM(model, return_sampling_mask=True,
logprobs_mode="processed_logprobs")
output = llm.generate(
"The capital of France is",
SamplingParams(temperature=1.0, top_k=50, top_p=0.95, logprobs=1),
)
mask = output[0].outputs[0].sampling_mask
# mask.token_ids: [[187, 326, 512], [42, 88], ...]
# mask.token_ids[i] = token IDs in the sampling support for generated token i
```

The mask is also available via the `/inference/v1/generate` HTTP endpoint:

```json
{
"choices": [{
"token_ids": [187, 42, 303],
"sampling_mask": [[187, 326, 512], [42, 88], [303, 11, 22]],
"finish_reason": "stop"
}]
}
```

## Requirements

| Requirement | Reason |
| --- | --- |
| `--return-sampling-mask` | Engine-level opt-in (disables FlashInfer sampler) |
| `--logprobs-mode processed_logprobs` | Returned logprobs are normalized over the nucleus, not full vocab |
| `temperature > 0` | Greedy has no truncated distribution |
| `top_k > 0` | Bounds mask size; pure top-p can produce vocab-sized masks |
| Model Runner V2 | Required by the async D2H copy pipeline |

The engine rejects unsupported combinations at startup or request time:

- Speculative decoding
- Diffusion models
- Custom logits processors (engine-level `--logits-processors`)

## How it works

1. The sampler applies all logit processors (penalties, logit bias, bad words,
temperature, min-p) and then top-k/top-p filtering, which sets excluded
logits to `-inf`.
2. After sampling, `torch.isfinite(processed_logits)` identifies the surviving
token IDs — this is the sampling mask.
3. The mask is transferred GPU → CPU asynchronously alongside sampled tokens.
4. On request completion, per-step masks are merged and converted to
`list[list[int]]` for the response.

## RL training usage

The training side needs two things for the importance ratio `π_θ/π_old`:

**`π_old(a|s)` — old policy's nucleus-normalized logprob:**
Already returned by vLLM when `--logprobs-mode processed_logprobs` is set.
The `log_softmax` is computed over processed logits (where filtered tokens
are `-inf`), so the denominator only includes the nucleus.

**`π_θ(a|s)` — current policy's nucleus-normalized logprob:**
Computed by the training framework using the mask:

```python
# mask_ids: list[int], the sampling support for this token
# logits: the training model's raw logits for this position
keep = torch.zeros(vocab_size, dtype=torch.bool)
keep[mask_ids] = True
masked_logits = logits.masked_fill(~keep, float("-inf"))
log_prob = log_softmax(masked_logits)[sampled_token_id]
```

Both sides normalize over the same token set, so the importance ratio is
consistent.

## Limitations

- **Engine-level flag:** `--return-sampling-mask` globally disables the
FlashInfer fused sampler. All requests pay the cost of the PyTorch sampling
path, even if they don't need the mask.
- **No streaming support:** The mask is returned only in the final response,
not in intermediate streaming chunks.
3 changes: 3 additions & 0 deletions rust/src/engine-core-client/src/protocol/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ pub struct EngineCoreOutput {
/// frontend grows one.
#[serde(default)]
pub mm_cache_miss_hashes: Option<Vec<String>>,
#[serde(default)]
pub new_sampling_mask: Option<OpaqueValue>,
}

impl EngineCoreOutput {
Expand Down Expand Up @@ -440,6 +442,7 @@ mod tests {
routed_experts: None,
num_nans_in_logits: 0,
mm_cache_miss_hashes: None,
new_sampling_mask: None,
},
],
scheduler_stats: None,
Expand Down
17 changes: 17 additions & 0 deletions rust/src/engine-core-client/src/tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2498,6 +2498,9 @@ fn python_msgpack_fixtures_match_rust_encoding() {
let defaults_request_hex = lines.next().expect("missing defaults request fixture line");
let multimodal_request_hex = lines.next().expect("missing multimodal request fixture line");
let outputs_hex = lines.next().expect("missing outputs fixture line");
let sampling_mask_outputs_hex = lines
.next()
.expect("missing sampling mask outputs fixture line");
let inline_logprobs_frames = lines.next().expect("missing inline logprobs fixture line");
let multipart_logprobs_frames = lines.next().expect("missing multipart logprobs fixture line");
let inline_prompt_frames = lines.next().expect("missing inline prompt logprobs fixture line");
Expand All @@ -2508,6 +2511,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
let request_bytes = hex::decode(request_hex).unwrap();
let multimodal_request_bytes = hex::decode(multimodal_request_hex).unwrap();
let outputs_bytes = hex::decode(outputs_hex).unwrap();
let sampling_mask_outputs_bytes = hex::decode(sampling_mask_outputs_hex).unwrap();

let decoded_request: EngineCoreRequest = rmp_serde::from_slice(&request_bytes).unwrap();
let expected_request = sample_request();
Expand Down Expand Up @@ -2571,6 +2575,18 @@ fn python_msgpack_fixtures_match_rust_encoding() {
decode_value(&rmp_serde::to_vec_named(&expected_multimodal_request.mm_features).unwrap());
assert_eq!(python_mm_features, rust_mm_features);

let decoded_sampling_mask_outputs: EngineCoreOutputs =
rmp_serde::from_slice(&sampling_mask_outputs_bytes).unwrap();
let sampling_mask_output = &decoded_sampling_mask_outputs
.as_request_batch()
.unwrap()
.outputs[0];
assert!(sampling_mask_output.mm_cache_miss_hashes.is_none());
assert!(matches!(
sampling_mask_output.new_sampling_mask.as_ref(),
Some(Value::Array(fields)) if fields.len() == 3
));

let decoded_outputs: EngineCoreOutputs = rmp_serde::from_slice(&outputs_bytes).unwrap();
expect_test::expect![[r#"
RequestBatch(
Expand Down Expand Up @@ -2598,6 +2614,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
routed_experts: None,
num_nans_in_logits: 0,
mm_cache_miss_hashes: None,
new_sampling_mask: None,
},
],
scheduler_stats: None,
Expand Down
25 changes: 25 additions & 0 deletions rust/src/engine-core-client/src/tests/python_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class EngineCoreOutput(
routed_experts: object | None = None
num_nans_in_logits: int = 0
mm_cache_miss_hashes: list[str] | None = None
new_sampling_mask: object | None = None


class EngineCoreOutputs(
Expand Down Expand Up @@ -202,6 +203,29 @@ class EngineCoreOutputs(
finished_requests={"req-1"},
)

sampling_mask_wire = [
[
"<i4",
[5],
msgpack.ExtType(3, np.array([2, 12, 16, 17, 18], dtype=np.int32).tobytes()),
],
[
"<i8",
[2],
msgpack.ExtType(3, np.array([0, 5], dtype=np.int64).tobytes()),
],
None,
]
outputs_with_sampling_mask = EngineCoreOutputs(
outputs=[
EngineCoreOutput(
request_id="req-mask",
new_token_ids=[16],
new_sampling_mask=sampling_mask_wire,
)
]
)


def encode_ndarray(
array: np.ndarray,
Expand Down Expand Up @@ -422,6 +446,7 @@ class EngineCoreReadyResponse:
print(msgspec.msgpack.encode(defaults_request).hex())
print(msgpack.packb(multimodal_request_wire, use_bin_type=True).hex())
print(msgspec.msgpack.encode(outputs).hex())
print(msgspec.msgpack.encode(outputs_with_sampling_mask).hex())
print(" ".join(frame.hex() for frame in encode_output_frames(inline_logprobs)))
print(
" ".join(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest_asyncio
from transformers import AutoTokenizer

import vllm.envs as envs
from tests.utils import RemoteOpenAIServer
from vllm.config import ModelConfig
from vllm.config.utils import getattr_iter
Expand Down Expand Up @@ -107,6 +108,49 @@ async def test_generate_endpoint(client):
resp.raise_for_status()
data = resp.json()
assert "choices" in data
assert data["choices"][0].get("sampling_mask") is None


@pytest.mark.asyncio
@pytest.mark.skipif(
envs.VLLM_USE_RUST_FRONTEND,
reason="sampling mask output is not supported by the Rust frontend",
)
@pytest.mark.parametrize(
"server",
[["--return-sampling-mask", "--logprobs-mode", "processed_logprobs"]],
indirect=True,
)
async def test_generate_sampling_mask(client):
top_k = 5
payload = {
"model": MODEL_NAME,
"token_ids": [1, 2, 3],
"sampling_params": {
"max_tokens": 5,
"temperature": 0.8,
"top_k": top_k,
"top_p": 0.9,
"ignore_eos": True,
"seed": 0,
},
"stream": False,
}
resp = await client.post(GEN_ENDPOINT, json=payload)
resp.raise_for_status()
choice = resp.json()["choices"][0]

token_ids = choice["token_ids"]
sampling_mask = choice["sampling_mask"]
assert sampling_mask is not None
assert len(token_ids) == len(sampling_mask)

vocab_size = get_vocab_size(MODEL_NAME)
for token_id, support in zip(token_ids, sampling_mask):
assert support
assert len(support) == len(set(support))
assert all(0 <= support_token_id < vocab_size for support_token_id in support)
assert token_id in support


@pytest.mark.asyncio
Expand Down
1 change: 1 addition & 0 deletions tests/v1/core/test_async_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def test_abort_request_when_structured_output_fsm_cannot_advance():
scheduler.vllm_config = Mock()
scheduler.vllm_config.model_config.enable_return_routed_experts = False
scheduler.enable_return_routed_experts = False
scheduler.return_sampling_mask = False
scheduler.recompute_kv_load_failures = False
scheduler.defer_block_free = False
scheduler.make_stats = Mock(return_value=None)
Expand Down
1 change: 1 addition & 0 deletions tests/v1/core/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3281,6 +3281,7 @@ def test_abort_request_when_structured_output_fsm_cannot_advance():
scheduler.vllm_config = Mock()
scheduler.vllm_config.model_config.enable_return_routed_experts = False
scheduler.enable_return_routed_experts = False
scheduler.return_sampling_mask = False
scheduler.recompute_kv_load_failures = False
scheduler.defer_block_free = False
scheduler.make_stats = Mock(return_value=None)
Expand Down
73 changes: 73 additions & 0 deletions tests/v1/test_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from unittest import TestCase

import numpy as np
import torch

from vllm.v1.outputs import LogprobsLists, LogprobsTensors
from vllm.v1.sample.ops.topk_topp_sampler import apply_top_k_top_p
from vllm.v1.worker.gpu.sample.output import SamplingMaskTensors


def test_logprobs_tensors_cat():
Expand All @@ -30,6 +33,76 @@ def test_logprobs_tensors_cat():
assert LogprobsTensors.cat([first]) is first


def test_sampling_mask_tensors_tolist():
tensors = SamplingMaskTensors(
packed_mask=torch.tensor(
[[0b00101000], [0b00000000], [0b10000000]],
dtype=torch.uint8,
),
counts=torch.tensor([2, 0, 1], dtype=torch.int32),
vocab_size=8,
)

result = tensors.tolists(np.array([1, 0, 1]))

assert result.token_ids.tolist() == [3, 5, 7]
assert result.offsets.tolist() == [0, 2, 3]
assert result.cu_num_generated_tokens == [0, 1, 1, 2]


def test_sampling_mask_lists_to_nested_list():
from vllm.v1.outputs import SamplingMaskLists

mask = SamplingMaskLists(
token_ids=np.array([10, 11, 12, 20, 21]),
offsets=np.array([0, 3, 5]),
)

nested = mask.to_nested_list()

assert nested == [[10, 11, 12], [20, 21]]


def test_sampling_mask_tensors_from_logits():
tensors = SamplingMaskTensors.from_logits(
logits=torch.tensor(
[
[1.0, float("-inf"), 2.0],
[3.0, 4.0, float("-inf")],
[float("-inf"), 5.0, 6.0],
],
device="cuda",
),
num_sampled_tokens=torch.tensor([1, 0, 1], device="cuda"),
)

result = tensors.tolists(np.array([1, 0, 1]))

assert result.token_ids.tolist() == [0, 2, 1, 2]
assert result.offsets.tolist() == [0, 2, 4]
assert result.cu_num_generated_tokens == [0, 1, 1, 2]


def test_sampling_mask_matches_processed_top_k_top_p_support():
processed_logits = apply_top_k_top_p(
logits=torch.tensor([[6.0, 5.0, 4.0, 4.0, 4.0, 2.0, 1.0, 0.0]], device="cuda"),
k=torch.tensor([3], device="cuda"),
p=torch.tensor([0.9], device="cuda"),
)
expected_token_ids = (
torch.isfinite(processed_logits[0]).nonzero().flatten().tolist()
)
assert 0 < len(expected_token_ids) < processed_logits.shape[1]

tensors = SamplingMaskTensors.from_logits(
processed_logits,
num_sampled_tokens=torch.tensor([1], device="cuda"),
)
result = tensors.tolists(np.array([1]))

assert result.to_nested_list() == [expected_token_ids]


class TestLogprobsLists(TestCase):
def setUp(self):
self.logprobsLists = LogprobsLists(
Expand Down
Loading
Loading