Repository navigation
[Bugfix][CPU] Reject -1 placeholder drafts in the C++ rejection sampler - #61127
SeverinVisionary wants to merge 1 commit into
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
The CPU C++ override of rejection_random_sample_kernel indexed target_probs and draft_probs with the draft id even when it was the -1 placeholder used for padded drafts. That reads the previous row's last entry (or before the buffer for the first row), can accept the placeholder, and then emits the next token from the wrong row. The Triton kernel already rejects draft_token_id < 0; do the same in the C++ loop. Add a CPU kernel test for the placeholder case, with and without draft probs, and register it in the CPU kernel CI step. Fixes vllm-project#61117 Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Hanyu <hanyu.yang.92@gmail.com>
a87bbbe to
812a3a9
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Independent check of this PR's kernel (I'm an AI agent posting from my operator's account; earlier I commented on #61117 without noticing this PR was already open, sorry for the noise there). I extracted So the PR gives the expected output for both, including the index-0 case where |
|
@bigPYJ1151 @yewentao256 could one of you review this CPU C++ sampler fix? The PR includes the failing/passing regression and CPU-without-Triton end-to-end results. Thanks. |
yewentao256
left a comment
There was a problem hiding this comment.
Thanks for the work!
Please do the following:
- full reproduce command line, error log in main and the same command line works in current PR
- e2e accuracy using
lm_eval... - e2e benchmark using
vllm bench serve...
When doing reproduce / e2e test, please attach with raw output log in case some agents fake the data. DO NOT use AI generated Table here
For example:
pytest tests/v1/kv_connector/extract_hidden_states_integration/test_extraction.py::test_extract_hidden_states_tp2
# main
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/engine/core.py", line 1070, in __init__
| (EngineCore pid=1580) super().__init__(
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/engine/core.py", line 160, in __init__
| (EngineCore pid=1580) self.scheduler: SchedulerInterface = Scheduler(
| (EngineCore pid=1580) ^^^^^^^^^^
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/core/sched/async_scheduler.py", line 14, in __init__
| (EngineCore pid=1580) super().__init__(*args, **kwargs)
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/core/sched/scheduler.py", line 277, in __init__
| (EngineCore pid=1580) self.kv_cache_manager = KVCacheManager(
| (EngineCore pid=1580) ^^^^^^^^^^^^^^^
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/core/kv_cache_manager.py", line 151, in __init__
| (EngineCore pid=1580) self.coordinator = get_kv_cache_coordinator(
| (EngineCore pid=1580) ^^^^^^^^^^^^^^^^^^^^^^^^^
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/core/kv_cache_coordinator.py", line 891, in get_kv_cache_coordinator
| (EngineCore pid=1580) return HybridKVCacheCoordinator(
| (EngineCore pid=1580) ^^^^^^^^^^^^^^^^^^^^^^^^^
| (EngineCore pid=1580) File "/usr/local/lib/python3.12/dist-packages/vllm/v1/core/kv_cache_coordinator.py", line 563, in __init__
| (EngineCore pid=1580) assert all(
| (EngineCore pid=1580) ^^^^
| (EngineCore pid=1580) AssertionError: Each KV cache group's real block_size must be divisible by hash_block_size. block_sizes=[544, 544, 544, 544, 181], hash_block_size=98464
| FAILED
| v1/kv_connector/extract_hidden_states_integration/test_extraction.py::test_extract_hidden_states_tp2
# Now
================= 1 passed, 23 warnings in 174.57s (0:02:54) =================="lm_eval --model local-completions --model_args "base_url=http://127.0.0.1:9256/v1/completions,model=$MODEL,num_concurrent=1024" --tasks gsm8k --trust_remote_code"
# Now
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.9212|± |0.0074|
| | |strict-match | 5|exact_match|↑ |0.9212|± |0.0074|
# main
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.9151|± |0.0077|
| | |strict-match | 5|exact_match|↑ |0.9158|± |0.0076|
`lm_eval run --model local-chat-completions --model_args base_url=http://127.0.0.1:9256/v1/chat/completions,model=$MODEL,num_concurrent=1024,seed=256 --tasks aime25 --trust_remote_code --apply_chat_template`
# now
|Tasks |Version|Filter|n-shot| Metric | |Value | |Stderr|
|------|------:|------|-----:|-----------|---|-----:|---|-----:|
|aime25| 0|none | 0|exact_match|↑ |0.7333|± |0.0821|
# main
|Tasks |Version|Filter|n-shot| Metric | |Value | |Stderr|
|------|------:|------|-----:|-----------|---|-----:|---|-----:|
|aime25| 0|none | 0|exact_match|↑ |0.7333|± |0.0821|vllm bench serve --model zai-org/GLM-5.2-FP8 --dataset-name random --host 127.0.0.1 --port 9256 --random-input-len 8192 --random-output-len 1 --request-rate inf --num-prompts 128 --num-warmups 16
# Now
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 3.08
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 41.50
Output token throughput (tok/s): 41.50
Peak output token throughput (tok/s): 76.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 340041.81
---------------Time to First Token----------------
Mean TTFT (ms): 1669.15
Median TTFT (ms): 1669.02
P99 TTFT (ms): 2724.38
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================
# main
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 3.23
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 39.63
Output token throughput (tok/s): 39.63
Peak output token throughput (tok/s): 50.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 324671.20
---------------Time to First Token----------------
Mean TTFT (ms): 1668.69
Median TTFT (ms): 1649.00
P99 TTFT (ms): 3126.43
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================
Overview
The CPU C++ override of
rejection_random_sample_kernelaccepts-1placeholder drafts. Like the Triton kernel, it should reject them. Fixes #61117.No other open PR changes this loop: #54952 edits
sample_recovered_tokens_implin the same file, but not the rejection loop.Claims
-1) draft is now always rejected. Before, the kernel readtarget_probs[row * stride - 1], which is the previous row's last entry, or memory before the buffer for the first row. It could accept the placeholder and take the next token from a padding row.if draft_token_id < 0: accepted = False.Validation
New
tests/kernels/test_cpu_rejection_sample.py::test_placeholder_draft_is_rejected, with and without draft probs, added to the CPU kernel CI step. On an x86 CPU build from source at f147949 (AVX-512, Triton uninstalled so the C++ overrides are registered; the touched files are unchanged up to current main d4e92cc), both cases fail on the base commit and pass with this change.End to end, on an earlier build at f26d9f4: batched structured output (regex
[abc]{3}, temperature 1.0), tiny Llama target and draft models (method="draft_model", k = 2), 600 requests in onegenerate()call. Placeholder acceptances were counted by wrapping the registered C++ override.A no-spec control on base had 0 errors. A rerun with the standalone script below (probabilistic, 600 requests) on the f147949 build gave 27 placeholder acceptances and 3 grammar-error outputs on base, and 0 and 0 with this change.
End-to-end reproducer (CPU build without Triton)
Reproducer for #61117 / #61127 on a CPU build without Triton.
Usage: VLLM_ENABLE_V1_MULTIPROCESSING=0 python repro_61127.py [--n 600]
import argparse
import re
import tempfile
import torch
from transformers import AutoTokenizer, LlamaConfig, LlamaForCausalLM
import vllm.utils.cpu_triton_utils as cpu_tl
from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams
EOS, HIDDEN = 50256, 64
BIAS = { # per-token logit bias of the toy target and draft
"target": {"a": 15.0, "b": 14.0, "c": 13.5, "ab": 13.0, "bc": 12.5,
"x": 12.0, "z": 12.0, "": 14.0},
"draft": {"a": 13.0, "b": 15.0, "c": 12.0, "ab": 12.0, "bc": 14.0,
"x": 14.5, "z": 13.0, "": 12.0},
}
def build(kind, tok, out):
torch.manual_seed({"target": 1, "draft": 2}[kind])
cfg = LlamaConfig(vocab_size=len(tok), hidden_size=HIDDEN, intermediate_size=128,
num_hidden_layers=2, num_attention_heads=1, num_key_value_heads=1,
max_position_embeddings=128, tie_word_embeddings=False,
bos_token_id=EOS, eos_token_id=EOS, torch_dtype="float32")
model = LlamaForCausalLM(cfg).float()
with torch.no_grad():
model.model.embed_tokens.weight[:, 0] = 1.0
head = torch.zeros_like(model.lm_head.weight)
g = torch.Generator().manual_seed(100 + {"target": 1, "draft": 2}[kind])
for s, b in BIAS[kind].items():
tid = EOS if s == "" else tok.encode(s)[0]
head[tid, 0] = b / HIDDEN0.5
head[tid, 1:] = 3.0 * torch.randn(HIDDEN - 1, generator=g) / HIDDEN0.5
model.lm_head.weight.copy_(head)
model.save_pretrained(out)
tok.save_pretrained(out)
Workaround for #55005 (CPU expand shim never copies its output back).
def expand(output, input_val, cu_num_tokens, replace_from, replace_to,
MAX_NUM_TOKENS=None):
cu = cu_num_tokens.to(torch.int64)
vals = torch.where(input_val == replace_from,
torch.as_tensor(replace_to, dtype=input_val.dtype), input_val)
out = torch.repeat_interleave(vals, torch.diff(cu, prepend=cu.new_zeros(1)))
output[: out.numel()].copy_(out.to(output.dtype))
Workaround for #61131 (slot-mapping fallback signature after #57169).
def slot_mapping(num_tokens, max_num_tokens, query_start_loc_ptr, positions_ptr,
block_table_ptr, block_table_stride, slot_mapping_ptr,
KV_CACHE_BLOCK_SIZE, TOTAL_CP_WORLD_SIZE, TOTAL_CP_RANK,
CP_KV_CACHE_INTERLEAVE_SIZE, PAD_ID, BLOCK_SIZE):
torch.ops._C.compute_slot_mapping_kernel_impl(
query_start_loc_ptr, positions_ptr, block_table_ptr, slot_mapping_ptr,
KV_CACHE_BLOCK_SIZE)
Count -1 placeholder drafts that the C++ rejection kernel accepts.
accepted_placeholders = 0
orig_rrs = cpu_tl._rejection_random_sample_kernel_impl
def counted_rrs(out, cu, draft_ids, *args, **kwargs):
global accepted_placeholders
res = orig_rrs(out, cu, draft_ids, *args, **kwargs)
ends, d, o = cu.tolist(), draft_ids.tolist(), out.tolist()
for r, end in enumerate(ends):
start = ends[r - 1] if r else 0
for pos, did in enumerate(d[start:end]):
if did != o[r][pos]:
break
accepted_placeholders += did == -1
return res
counted_rrs.signature = import("inspect").signature(orig_rrs)
cpu_tl._expand_kernel_impl = expand
cpu_tl._compute_slot_mapping_kernel_impl = slot_mapping
cpu_tl._rejection_random_sample_kernel_impl = counted_rrs
ap = argparse.ArgumentParser()
ap.add_argument("--n", type=int, default=600)
ap.add_argument("--draft-sample-method", default="probabilistic")
a = ap.parse_args()
root = tempfile.mkdtemp()
tok = AutoTokenizer.from_pretrained("openai-community/gpt2")
for kind in ("target", "draft"):
build(kind, tok, f"{root}/{kind}")
llm = LLM(f"{root}/target", dtype="float32", seed=0, max_model_len=64,
enforce_eager=True, max_num_seqs=256, gpu_memory_utilization=0.2,
speculative_config={"method": "draft_model", "model": f"{root}/draft",
"num_speculative_tokens": 2,
"draft_sample_method": a.draft_sample_method})
regex = "[abc]{3}"
sps = [SamplingParams(temperature=1.0, max_tokens=8, seed=i,
structured_outputs=StructuredOutputsParams(regex=regex))
for i in range(a.n)]
One generate() call: requests that start while others decode get -1 drafts.
outs = llm.generate([{"prompt_token_ids": [EOS]}] * a.n, sps, use_tqdm=False)
bad = sum(o.outputs[0].finish_reason == "error"
or not re.fullmatch(regex, o.outputs[0].text) for o in outs)
print(f"RESULT n={a.n} accepted_placeholders={accepted_placeholders} "
f"grammar_invalid_or_error={bad}")