Skip to content

5080deploy: merge upstream #224, OpenAI-compatible logprobs - #63

Open
gdevenyi wants to merge 7 commits into
5080/24-request-metrics-504from
5080/25-logprobs-224
Open

gdevenyi wants to merge 7 commits into
5080/24-request-metrics-504from
5080/25-logprobs-224

Conversation

@gdevenyi

Copy link
Copy Markdown
Owner

Stacked on #62. Merges upstream FlashML-org#224 (head 9a48a4b): OpenAI-compatible logprobs -- raw (pre-temperature) sampled-token logprobs plus top alternatives. Before it, /v1/completions rejected logprobs and chat returned "logprobs": null.

What this deployment gets: /v1/completions with logprobs: 0..5, streaming and not, with n > 1. Chat logprobs are refused on this server by the PR's design: it fails closed with a 400 whenever a reasoning parser is active (hidden reasoning tokens cannot be aligned with visible content), and production always runs the qwen3 reasoning parser. The error message points clients to /v1/completions.

Merge: conflicts in 10 files against #47 (sampler logits-processor plan, n > 1 fan-out, echo/suffix, logit_bias, detokenizer reasoning-token counts), upstream FlashML-org#471 (greedy mask) and FlashML-org#504. Resolutions to review:

Testing (RTX 5080, production flags):

  • Live: completions logprobs: 3 -> tokens [' Paris', '.', ' The'], logprobs [-0.578, -0.538, -1.232], offsets [0, 6, 7], three top alternatives per token; the streaming variant returns the same tokens; n: 2 returns a logprob list per choice; logprobs: 9 -> 400; chat + logprobs -> 400 with the redirect message; plain chat unchanged (no logprobs key).
  • tests/server tests/scheduler tests/engine tests/tokenizer: 1207 passed.
  • pytest tests/ -m "not slow" at the top of this stack (77d8d5c): 2446 passed, 206 skipped; the 4 failures are the known environment-only ones (3x test_muse_glimmer_vision.py on transformers 5.15.1, test_async_pull_coalesced needing pytest-asyncio).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

Artemowka22 and others added 7 commits August 26, 2026 23:23
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.
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>
The logprobs change moved the drain from positional unpacking to named
ForwardOutput attributes (next_tokens_cpu, copy_done_event, the *_cpu
logprob columns), which the bare-tuple fake in the in-flight-abort
scheduler tests predates: three of them died with 'tuple' object has no
attribute 'next_tokens_cpu' on a GPU run. Caught by gdevenyi on the PR
thread; a macOS run cannot see it (the file already fails on the missing
kernel packages there, identically on both sides of the series).

A namespace mirroring the drain-facing fields keeps the test off the
engine import chain, whose kernel packages a scheduler unit test should
not require.

Assisted-by: Claude
Two conflicts:

- python/freetoken/server/openai_api.py: upstream threads the configurable
  default output budget (default_max_tokens, FlashML-org#411) into the resolve_sampling
  call that this branch had extracted into a variable to attach the logprobs
  fields. Kept the variable; its construction now passes default_max_tokens.
- python/freetoken/tokenizer/server.py: import conflict between upstream's
  get_mm_processor (image input, FlashML-org#454) and this branch's build_logprobs_entry.
  Kept both.

Assisted-by: Claude
…hat and legacy completions

# Conflicts:
#	python/freetoken/core.py
#	python/freetoken/engine/engine.py
#	python/freetoken/engine/sample.py
#	python/freetoken/message/frontend.py
#	python/freetoken/message/tokenizer.py
#	python/freetoken/scheduler/scheduler.py
#	python/freetoken/server/generation.py
#	python/freetoken/server/openai_api.py
#	python/freetoken/tokenizer/detokenize.py
#	python/freetoken/tokenizer/server.py
…lag the sampler plan reads

Fork #47's sampler plan asks each request whether it needs logits processing; the
upstream FlashML-org#224 test fakes predate it.

Assisted-by: Claude Opus 5.5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants