Repository navigation
seed and chat logprobs are silently ignored (200 with no effect); proposal for per-request seeded sampling #693
Description
What happened
Two OpenAI request fields are ignored without an error, so the client gets a 200 response that does not do what it asked for:
| Request | Expected | Got |
|---|---|---|
/v1/chat/completions, temperature: 1.0, seed: 1234, the same request twice |
the same output | different output |
/v1/completions, temperature: 1.0, seed: 7, the same request twice |
the same output | different output |
/v1/chat/completions, logprobs: true, top_logprobs: 3 |
choices[].logprobs.content[] |
"logprobs": null, no error |
/v1/completions, logprobs: 2 |
logprobs, or a 400 | 400 logprobs is not supported (correct, but it shows the gap) |
temperature: 0 gives the same output each time in my tests (single short requests, thinking off). Other sampling fields work: top_p, top_k, min_p, the penalties, stop and max_tokens.
Minimal reproduction:
for i in 1 2; do curl -s localhost:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model": "Qwen3.8-Flash-Next", "max_tokens": 20, "temperature": 1.0, "seed": 1234,
"chat_template_kwargs": {"enable_thinking": false},
"messages": [{"role": "user", "content": "Invent a name for a cat."}]}' | jq -r '.choices[0].message.content'; doneCause (current main, 0bca88f)
server/api_models.py:ChatCompletionRequesthasmodel_config = ConfigDict(extra="allow")and noseedorlogprobsfield. Both are accepted and then dropped.CompletionRequestalso has noseed, but it rejectslogprobs(openai_api.pyaround line 658).SamplingParamshas no seed, andengine/sample.pysample_impl()calls the sampling functions withoutseed/offset. The Triton sampler (kernel/triton/sampling.py,_gen_u) already acceptsseedandoffset, but only one value for the whole batch.
Proposed fix
seed:
- Add
seed: int | NonetoChatCompletionRequest,CompletionRequestandSamplingParams, and carry it to the scheduler like the other sampling fields. - In
Sampler.prepare(), build a per-row(seed, offset)pair for the rows that have a seed. Use the number of generated tokens as the offset, so each step draws a new number but a replay draws the same one. - Make the uniform draw per row: in the Triton path,
tl.rand(seed_row, offset_row)(Philox) inside the kernel, or one generator per seeded row in_gen_u. Rows without a seed keep the current unseeded draw. A batch with no seeded rows keeps exactly the current path. If the FlashInfer sampling path cannot take per-row seeds, send batches that contain a seeded row to the Triton path. - Document the limit: a seed makes the draw reproducible, but the logits can still differ between batch compositions (see temperature 0 is not reproducible with thinking on (identical requests diverge on near-ties) #307 for the near-tie case at temperature 0). So the guarantee is "best effort", like OpenAI's own
seed.
logprobs:
#224 implements logprobs / top_logprobs for both routes, with raw pre-temperature values and no cost when not requested. It has been rebased and tested by several people. Merging it closes this part. Until then, /v1/chat/completions should return 400 for logprobs: true, like /v1/completions already does, and not null.
In general: extra="allow" on the request models turns every unsupported OpenAI field into a silent no-op. A short deny-list of fields that change the result (seed, logprobs, top_logprobs, logit_bias, n (see #682), response_format) that returns 400 until each one is implemented would make these gaps visible to clients.
Environment
- Built from source: upstream
main2d8b9cc plus a local deploy stack. I checked each code path above on currentmain0bca88f, and none of it has changed since. - Ubuntu 24.04, Python 3.12, 2x RTX 6000 Ada 48 GB (driver 595.91), TP=2, 503 GB RAM.
- Checkpoint
nvidia/Qwen3.8-Flash-Next-NVFP4. - All results above are reproducible directly against
ft serve, with no proxy.
Activity
SUKUNA-AI commentedon Oct 19, 2026
We implemented the
seedpart of this proposal in a fork of FreeToken 0.1.3 (single RTX 5070 Ti, FlashInfersampler), in case the details help a PR.
Still present on main 4d2e2e2:
ChatCompletionRequesthasextra="allow"and noseed(server/api_models.py:63-64).engine/sample.py:41-52calls the sampling functions withoutseed/offset.What we did (a small diff against 0.1.3;
server/api_models.py,server/generation.py,server/openai_api.py,core.py,engine/sample.py):seed: int | NoneonChatCompletionRequestandSamplingParams, passed throughresolve_samplinglike theother fields.
Sampler.preparederives one 63-bit key per seeded row with a SplitMix64 mix of(seed, req.device_len). The keydepends on the absolute token position, not on a step counter, so a replay draws the same number at the same
position wherever the step boundaries fall. That includes resuming from a cached prefix.
Sampler.sample: if any row has a seed, every row is sampled on its own withseed=key, offset=0passed to theFlashInfer
*_sampling_from_probsfunctions. A batch without seeded rows takes exactly the old path. A seed on abuild without FlashInfer raises, instead of silently falling back to an unseeded draw.
Limitations of ours:
--max-running-requests 1, but we didnot measure it for batches. A per-row Philox key inside the Triton kernel, as you suggest, would avoid it.
keys its fixtures by request seed), but that does not test the sampled draw.
Samplerhas gained penalties (fix(sampling): apply presence and frequency penalties #527) and a greedy mask (fix(engine): preserve greedy sampling in mixed batches #471) since 0.1.3, so our patch would need a rebase.We are happy to turn this into a PR on top of current main, or to review one.
Our implementation, for reference (fork of 0.1.3, Apache-2.0; paths in the fork):
server/api_models.pyserver/generation.pyserver/openai_api.pycore.pyengine/sample.py