Skip to content

seed and chat logprobs are silently ignored (200 with no effect); proposal for per-request seeded sampling #693

Open

Description

@gdevenyi

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'; done

Cause (current main, 0bca88f)

  • server/api_models.py: ChatCompletionRequest has model_config = ConfigDict(extra="allow") and no seed or logprobs field. Both are accepted and then dropped. CompletionRequest also has no seed, but it rejects logprobs (openai_api.py around line 658).
  • SamplingParams has no seed, and engine/sample.py sample_impl() calls the sampling functions without seed / offset. The Triton sampler (kernel/triton/sampling.py, _gen_u) already accepts seed and offset, but only one value for the whole batch.

Proposed fix

seed:

  1. Add seed: int | None to ChatCompletionRequest, CompletionRequest and SamplingParams, and carry it to the scheduler like the other sampling fields.
  2. 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.
  3. 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.
  4. 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 main 2d8b9cc plus a local deploy stack. I checked each code path above on current main 0bca88f, 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

  1. SUKUNA-AI commented on Oct 19, 2026

    @SUKUNA-AI

    We implemented the seed part of this proposal in a fork of FreeToken 0.1.3 (single RTX 5070 Ti, FlashInfer
    sampler), in case the details help a PR.

    Still present on main 4d2e2e2: ChatCompletionRequest has extra="allow" and no seed (server/api_models.py:63-64).
    engine/sample.py:41-52 calls the sampling functions without seed/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 | None on ChatCompletionRequest and SamplingParams, passed through resolve_sampling like the
      other fields.
    • Sampler.prepare derives one 63-bit key per seeded row with a SplitMix64 mix of (seed, req.device_len). The key
      depends 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 with seed=key, offset=0 passed to the
      FlashInfer *_sampling_from_probs functions. A batch without seeded rows takes exactly the old path. A seed on a
      build without FlashInfer raises, instead of silently falling back to an unseeded draw.

    Limitations of ours:

    • Sampling row by row costs one sampler launch per row. That is fine for our --max-running-requests 1, but we did
      not measure it for batches. A per-row Philox key inside the Triton kernel, as you suggest, would avoid it.
    • No Triton-path implementation.
    • We have no dedicated reproducibility test. Our teacher-forcing harness depends on the seed reaching the engine (it
      keys its fixtures by request seed), but that does not test the sampled draw.
    • main's Sampler has 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.
    • As you note, a seed makes the draw reproducible, not the logits.

    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):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions