Repository navigation
fix(sampling): reject oversized top_k, logprobs, and n before they kill the server - #42779
cipheraxat wants to merge 41 commits into
Conversation
…ll the server A huge top_k overflows the int32 tensor, a huge logprobs width blows up torch.topk, and a huge n copies the prompt before verify runs.
The crash fix only needs the fan-out cap. Beam search already does not copy the prompt, so it skips that cap instead of growing a second n check.
Beam search skips the fan-out cap, but n is later used as a slice. Zero or a negative n must 400 before it can drop sequences.
|
/tag-and-rerun-ci |
…res (sgl-project#42663) Co-authored-by: Kan Wu <kan.wu@radixark.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…memory (sgl-project#42754) Co-authored-by: merrymercy <merrymercy@users.noreply.github.com> Co-authored-by: xiezhq-hermann <xiezhq-hermann@users.noreply.github.com>
| n = self._coerce_parallel_n(self.sampling_params.get("n", 1)) | ||
| else: # isinstance(self.sampling_params, list): | ||
| self.parallel_sample_num = self.sampling_params[0].get("n", 1) | ||
| n = self._coerce_parallel_n(self.sampling_params[0].get("n", 1)) |
There was a problem hiding this comment.
why validate only the first request?
There was a problem hiding this comment.
Every request in the batch is checked now, with its own beam_width. A beam item at the front no longer lets a later one skip the cap.
|
|
||
| # Build return object | ||
| if isinstance(obj, GenerateReqInput): | ||
| check_top_logprobs_num(obj.top_logprobs_num, self.model_config.vocab_size) |
There was a problem hiding this comment.
doesn't belong here, should be adjacent to token_ids_logprob validation
There was a problem hiding this comment.
Moved it next to the token_ids_logprob check.
| def check_n(n: Any) -> None: | ||
| """Reject a parallel-sample count that would replicate the prompt.""" | ||
| if type(n) is not int or not 1 <= n <= MAX_N: | ||
| raise ValueError(f"n must be an integer in [1, {MAX_N}], got {n}.") | ||
|
|
||
|
|
||
| def check_beam_n(n: Any) -> None: | ||
| """Reject a non-positive beam n. | ||
|
|
||
| Beam search does not replicate the prompt, so n is not capped at MAX_N. | ||
| It is stored as num_return and used as a slice, so 0 drops every sequence | ||
| and a negative n drops from the end. | ||
| """ | ||
| if type(n) is not int or n < 1: | ||
| raise ValueError(f"n must be an integer >= 1, got {n}.") | ||
|
|
||
|
|
||
| def check_top_logprobs_num(value: Any, vocab_size: int) -> None: | ||
| """Reject a top-logprobs width that torch.topk cannot serve for this vocab.""" | ||
| if type(value) is not int or not 0 <= value <= vocab_size: | ||
| raise ValueError( | ||
| f"top_logprobs_num must be an integer in [0, vocab_size({vocab_size})], " | ||
| f"got {value}." |
There was a problem hiding this comment.
there are 5 validators for this field in this pr (5 python, 2 rust), do we need so many?
There was a problem hiding this comment.
Yeah, that was too many. There is one check_n now. It runs before the prompt is copied, and again in verify. Rust already rejects n != 1, so I left that as is.
| return | ||
| elif isinstance(self.sampling_params, dict): | ||
| self.parallel_sample_num = self.sampling_params.get("n", 1) | ||
| n = self._coerce_parallel_n(self.sampling_params.get("n", 1)) |
There was a problem hiding this comment.
please extract sampling_params.get("n", 1) once instead of nesting it in every call
There was a problem hiding this comment.
Done. n is read once per request, then the null case is handled
…egaMoE MMA type (sgl-project#42022) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…DA image (sgl-project#42612) Co-authored-by: Mohammad Angkad <mohammad.angkad@radixark.ai>
…gl-project#42651) Co-authored-by: merrymercy <merrymercy@users.noreply.github.com> Co-authored-by: 842974287 <842974287@users.noreply.github.com>
Co-authored-by: merrymercy <merrymercy@users.noreply.github.com> Co-authored-by: charlotte12l <charlotte12l@users.noreply.github.com>
…ct#41765) Co-authored-by: Po-Han Huang (NVIDIA) <53919306+nvpohanh@users.noreply.github.com>
…oject#42625) Co-authored-by: Cheng Wan <54331508+ch-wan@users.noreply.github.com>
…roject#39659) Signed-off-by: jain-ria <riajain@NVIDIA.com> Co-authored-by: ishandhanani <82981111+ishandhanani@users.noreply.github.com>
Motivation
SamplingParams.verifyrange-checks almost every sampling field, but it never checks an upper bound ontop_k, and it does not checknat all.top_logprobs_numis not aSamplingParamsfield, so it skipsverifyentirely. The values then reach torch inside the scheduler, the event loop dies, and the server stops answering/health.A
/v1/completionsrequest withmax_tokens: 4does this:"top_k": 2147483648overflows the int32top_kstensor (value cannot be converted to type int32 without overflow)"logprobs": 2147483648is passed totorch.topkpast the vocab row (selected index k out of range)"n": 2147483648copies the prompt insidenormalize_batch_and_arguments, which runs beforeverify, so the request times out and the server stops answeringThe sibling fields already 400.
min_new_tokens, a negativetop_k, and a non-finitefrequency_penaltyall come back fromverifywithout killing the process.Tracked in #41482.
Reproduced with:
python -m sglang.launch_server --model-path Qwen/Qwen3-0.6B --port 30000 python repro_sglang_sampling_upper_bound.py --base-url http://127.0.0.1:30000 --variant top_k # one variant per fresh server: top_k, logprobs, nModifications
Upper bounds in
sampling_params.py,io_struct.py, andtokenizer_manager.py:top_kmust be-1(whole vocabulary, stored asTOP_K_ALL = 1 << 30) or in[1, vocab_size].TOP_K_ALLstill fits in the int32top_kstensor. Both Rustverifycopies use the same check, and a negativetop_kis rejected before theu64cast.nfor normal sampling must be in[1, 128]. The check runs in_handle_parallel_samplingbefore any list replication, and again inverify. An explicit JSONnullstill means one sample, same as leavingnout.beam_width > 1the 128 cap is skipped and fan-out stays 1.nmust still be an integer>= 1:0would return no sequences, and a negativenwould slice from the end.nabovebeam_widthis still rejected later by the beam coordinator.top_logprobs_nummust be an integer in[0, vocab_size], checked when a generate request is tokenized.k == vocab_sizeis allowed, so a reranker asking for 50 still passes. The Rust frontend checks the same bound in scheduler validation, which is the first place that knows the vocab size.Deliberately unchanged:
top_k=-1still disables the limittop_logprobs(the bound is the vocab size)n != 1n <= beam_widthcheckTests
Covers
top_katvocab_sizeand atTOP_K_ALL,top_kpast the vocab and past int32,nat 1 and 128,nof 0 and above 128, beamnabove 128 that does not copy the prompt, beamnof 0 and -1, beamnabovebeam_widththat still passesverify, a JSONnullnthat stays one prompt, and a tokenized generate request whosetop_logprobs_numis past the vocab.Rust:
verifyacceptstop_k == vocaband rejectstop_k == vocab + 1in both copies.top_logprobs_num_is_bounded_by_vocabcallsvalidate()directly: 1001 against a vocab of 1000 returns the validation error, andk == vocab_sizeis accepted.This environment has no Triton and no
cargo, so the pytest files were not collected here and the Rust tests were not executed. TheSamplingParamsbounds were checked by importing that module directly.Accuracy Tests
N/A: request validation only. Values that were already legal stay legal.
nabove 128 now returns 400. A beam request does not copy the prompt, and a beamnbelow 1 returns 400.nabovebeam_widthis still the coordinator's error.Speed Tests and Profiling
N/A. The new checks are a few integer comparisons on the request path.
Checklist
sampling_params.mdxnow states thetop_k,top_logprobs_num, andnranges.CI States
Latest PR Test (Base): ❌ Missing
run-cilabel -- add it to run CI tests.Latest PR Test (Extra): ❌ Blocked --
run-ciis required first.Latest PR Test (AMD ROCm 10): ➖ No AMD PR run found for this commit.