feat(server): OpenAI-compatible logprobs for chat and legacy completions - #224
feat(server): OpenAI-compatible logprobs for chat and legacy completions#224Artemowka22 wants to merge 3 commits into
Conversation
Adds the engine half of OpenAI logprobs support. SamplingParams gains logprobs/top_logprobs; the sampler computes log_softmax over the PRE-temperature logits (raw model distribution, so temperature/top-k/top-p do not change reported values), gathers the chosen token and batch-max top-k, and ships CPU copies covered by the existing copy_done_event. The scheduler attaches per-request values (cut to each request's own top_logprobs) to DetokenizeMsg; the detokenizer builds a neutral entry (token text via single-id decode + UTF-8 bytes, so clients can reassemble partial-UTF-8 pieces) onto UserReply.logprobs. Zero cost when off: no row asked -> no mask tensor, no log_softmax, ForwardOutput carries None. Message fields default to None, so old and new peers interoperate. Stop-string trimming can hide final visible text; entries still cover every sampled token.
API half of logprobs support. Chat: logprobs + top_logprobs (0..20) yield
choice.logprobs.content entries {token, logprob, bytes, top_logprobs[]},
streaming and non-streaming. Completions: the legacy integer field (0..5) yields
{tokens, token_logprobs, top_logprobs, text_offset} with absolute offsets across
the stream; echo+logprobs stays rejected (prompt logprobs need prefill logits
and are out of scope here).
The protocol-neutral event layer carries entries on ContentDelta (a list -- parser
buffering can release several tokens' text in one delta); entries always
accumulate on GenResult for the non-streaming path, and reasoning/tool-call
buffering carries pending entries onto the next content delta. Formatting lives
in server/logprobs.py; wire compatibility follows the OpenAI shapes.
|
Fresh compatibility check from current |
|
RTX 5090 / torch 2.11 cu130 microbenchmark on Qwen3.8 vocab size 248,320: the PR implementation shape ( |
|
One correctness issue before merge: streaming semantic parsing currently misattributes hidden-token logprobs to later visible content. In |
Review finding on FlashML-org#224 (HaileyStorm): streaming chat carried logprob entries of hidden reasoning tokens onto the next visible content delta -- pending entries accumulated before the reasoning parser classified the text, and _content_delta() drained the whole list into whatever visible chunk came next. That broke token<->content alignment and leaked hidden reasoning token strings through logprobs entries; non-streaming meanwhile kept every sampled entry and end-of-stream dropped undrained ones -- three behaviors for one surface. Close it at both layers: - /v1/chat/completions rejects logprobs=true with a 400 when the server runs a reasoning parser or the request enables tool parsing: the semantic layer can hide or reclassify tokens, so entries cannot be aligned 1:1 with visible content tokens. tool_choice="none" keeps logprobs available; the raw /v1/completions path (no semantic layer) is unchanged. - _generate_events_impl now collects entries only on the passthrough path (no reasoning parser, no tool parsing), so a hidden-token entry can never ride a later visible delta even for a caller that skips API validation. Replaces test_reasoning_logprob_is_carried_to_next_content_delta (which enshrined the carry) with fail-close coverage for both conflict cases, the tool_choice="none" pass-through, and a no-carry guard test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Confirmed and fixed — thanks for the precise diagnosis (and for the perf check and for deferring your own implementation). You were right on all three counts: entries were appended before the reasoning parser classified the ack's text,
The carry test is replaced by a no-carry guard test plus fail-close coverage for both conflict cases and the Exact token-aware routing (each public entry emitted only with its own public content token) would need the reasoning-parser interface to report per-token consumption; happy to do that as a follow-up if maintainers want logprobs and reasoning to coexist on chat. |
…gprobs for chat and legacy completions Upstream FlashML-org#224 at 855650d, merged onto deploy/chatdnp for the PR sweep. Conflicts: engine.py keeps FlashML-org#231's stats readout before the logprobs-aware return; openai_api.py keeps the vision `images` argument and FlashML-org#222's disconnect-watching drain with the logprobs entries added; generation.py keeps FlashML-org#266's marker filter and routes every content delta through FlashML-org#224's _content_delta so the logprobs entries ride the filtered text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173pf9k9fSVtwbm3f898HDt
…ollows ForwardOutput (FlashML-org#224) The donate and hit-admission barriers of PR FlashML-org#287 call torch.cuda.synchronize on the cache manager's device; the scheduler unit tests run that path on a CPU device, so guard the call on device.type. The drained-forward fake in test_abort_inflight_prefill built a bare tuple, which PR FlashML-org#224's scheduler no longer unpacks: build a ForwardOutput instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173pf9k9fSVtwbm3f898HDt
|
Tried on 2 x RTX 6000 Ada (sm_89) serving Qwen3.8-Flash-Next (RadixArk NVFP4) at TP=2, offload backend, fp8 KV pool of 8 x 262,144 tokens, merged onto my deploy branch (main af71ba4 + #385/#386/#389/#392/#354 and ten other open PRs), tests run on the box, then put in production. Merged with conflicts against #231 (engine.py), #222 (openai_api.py) and #266 (generation.py, the content deltas now go through
|
Summary
Implements
logprobsfor sampled tokens across the whole pipeline; today/v1/completionsrejects the field ("logprobs is not supported") and/v1/chat/completionssilently swallows it (extra="allow").logprobs: true+top_logprobs: 0..20→ each choice carrieslogprobs.content[]entries{token, logprob, bytes, top_logprobs[]}, streaming and non-streaming.logprobs: 0..5→{tokens, token_logprobs, top_logprobs, text_offset}.echotogether withlogprobsstays rejected (prompt logprobs need prefill logits and are out of scope here).Design
log_softmaxover the pre-temperature logits, so temperature/top-k/top-p do not change what is reported (matches vLLM's default) and greedy eval harnesses get the true model distribution.log_softmax+topk+ a small D2H copy that rides the existingcopy_done_event.DetokenizeMsg.chosen_logprob/top_ids/top_logprobs,UserReply.logprobs), all defaulting to None — wire-compatible with older peers.tokenizer.decode([id])); thebytesfield carries UTF-8 so clients can reassemble partial-UTF-8 pieces, same trade-off OpenAI documents.Why
Any evaluation gate worth trusting (teacher-forced agreement/KL against a reference checkpoint, perplexity tracking of quantized variants) needs token logprobs from the OpenAI endpoint; with the radix prefix cache, per-position 1-token continuation calls make teacher-forced scoring practical without echo support.
Test plan
tests/engine/test_sample_logprobs.py) and the entry builder (tests/tokenizer/test_logprobs_entry.py)tests/server/test_logprobs_api.py) — no GPU, no weights, no network