Repository navigation
Conversation
Thanks for this — it is exactly what we needed for typed decisions (one token, probability per option letter), the use case of #675. Measured on 2× RTX 3090 (24 GB each), layer split
Why: with A small fix on top of your branch (server only, engine unchanged), +57/−8 in
Checked: all Not covered / not tested: logprobs for the slot phase ( Would you like this as a PR into |
|
@karls0r thank you for the measurements and the fix, and sorry for the slow reply. Please open it as a PR into Refusing Once it's in, I'll rebase the branch onto current main (it is behind) and run the serve tests again. |
|
Thank you. A feature, outside this speed pass. |
|
Another data point for this PR, plus a rebase onto v0.1.45 in case it saves time. Rebase. This branch (
Measured. RTX 3090 (24 GB, sm_86, CUDA 13.1), Linux, Qwen3.8-Flash-Next GSQ-RCO Q2_0,
One more thing we needed for vLLM-shaped decision clients was |
- engine: the GEN key logprobs=K (0..20) writes "LP logprob id:logprob ..." after each "T id", from the verify window's head logits (as the SYCL port's 835d30b); absent key, no LP lines - serve: generate() reads a token's LP line before yielding the token; each text chunk carries the logprobs of the tokens that spell it (markup such as </think> left out, a character split across tokens in the token that finishes it); openai_collect merges them into choices[0].logprobs; a json_schema answer is returned as written when its tokens spell it - tests: serve/test_logprobs.py; docs/DETAILS.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; 400 for longer replies With "parallel" set, generate() goes to generate_batched(), whose control-line reader (_control) had no LP handling: the engine wrote the LP line (BGEN is read as GEN 1, logprobs=K included) and the server dropped it, so every reply came back with "logprobs": null. - _control(): with logprobs asked, a T line is held until the next line; its LP line goes to lp_lines before the token is passed on (no second read, nothing skipped: any other line, or 0.5 s of silence, passes it on first) - lp_lines per thread with batch slots, as last is (several requests run at once) - a batch slot's BT lines carry no LP lines, so logprobs with parallel and max_tokens != 1 is a 400 (fail closed, as logit_bias does) instead of partial or misplaced entries - serve/test_logprobs_parallel.py: the fake engine of test_parallel with LP lines; one decision, four at once (each gets its own), not asked, longer replies refused, an engine without LP lines Contributed by an AI agent (Ren, Claude) on behalf of karls0r; measured on 2x RTX 3090, layer split.
…t for replicas without them ReplicaEngine.batch counts each replica without batch slots as one, so with replicas and "parallel" off, logprobs with max_tokens above 1 got the 400 that says "run without parallel". batch_slots_on() asks each replica's own engine. Tests: test_logprobs.Replicas (a 200-token reply with logprobs through two replicas without slots; the 400 once one has them). docs/DETAILS.md: the one-token limit with "parallel", with @karls0r's measurement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@settinghead thank you for the rebase and the RTX 3090 numbers. The 196-request test, run in both option orders, is a good check for this PR's main use. The branch is now on v0.1.45 (4098d58), with @karls0r's fix merged in. I resolved the same two conflicts the same way you did: the Checked on a Radeon 8060S (HIP, Windows, UD-IQ4_XS,
For anyone testing on Strix Halo: v0.1.45 can stall on the first prompt there with gfx1151's default switches on. #3151 has the fix; until it lands, |
|
Thanks for the pull request. We're resetting the tracker for 0.2.0-beta. There are more than 1,300 open issues and pull requests, too many for us to review properly, so we're closing everything that isn't being worked on right now. Closed here does not mean rejected. To reopen it takes about two minutes:
The rules are in CONTRIBUTING.md. |
…plica server without slots A batch slot prints "BT <slot> <id>" and its decode loop never calls emit_lp_line, so logprobs was refused outright there. An admission does write one: a max_tokens 1 request is asked of the engine as BGEN and read as "GEN 1" (generate.cpp:12090), the single-token path that prints "T <id>" and the LP line after it in one flush. _control() now reads that line (want_lp) and keeps it on the request's own LPLines, held on its cancel Event -- the engine's lp_lines is one attribute and several slots read control lines at once, so it cannot be shared between them. A longer reply is still a 400: past that first token its entries would cover a prefix only, which a client cannot tell from the model's own answer. This is the line gt50 drew on Niko1221#1916 ("LP lines for the BT slot phase can be a later step"), and the read itself follows the server-only fix karls0r posted there (+57/-8, engine unchanged). The other half is a refusal that should not have been there at all. ReplicaEngine.batch totals how many requests its engines run at once and counts a replica without slots as one (serve/replicas.py), and the check asked it "does this engine use batch slots". A server started with --replicas 2 and no --parallel therefore answered 400 to every logprobs request, while reading every reply by the GEN path, which does write the lines. batch_slots_on() asks each replica's own figure instead. Tests: serve.test_logprobs 32/32 -- the two new ones in Replicas are no slots behind two replicas, and slots on one of them; serve.test_logprobs_parallel 6/6 -- a decision, allowed_token_ids on the admission, four decisions at once each with its own, not asked, a longer reply a 400, and an engine that writes no LP lines at all. Hardware, Swift-1.5 IQ3_XXS, --spec 4 --mtp, --parallel 2: max_tokens 1 + top_logprobs 5 -> 200, one entry spelling the reply (A -0.062427, then B -2.848, " A" -7.415); allowed_token_ids over 11 ids -> 200, the named ids as the candidates, sum(exp) 1.0, a chosen token outside them keeping its own logprob; max_tokens 5, max_tokens absent and max_completion_tokens 0 -> 400 naming max_tokens 1; max_completion_tokens 1 -> 200 with its entry; streamed -> one chunk carrying the entry for "Odd"; four decisions at once on two slots -> 200 each, 1.6 s for all four, every entry matching its reply; a plain request and a 64-token reply unchanged. The whole serve suite: 1073 tests, 1 failure, 1 error, 21 skipped. Both are known on a clean upstream/main here and touch no logprobs code: test_responses.test_server_close_ends_the_sampler (1 != 2, order-sensitive -- it passes when its file runs alone, which it did in the 83-test run just before this one) and test_winjob's running() on a pid this environment cannot read.
Title
OpenAI logprobs on /v1/chat/completions for the CUDA/HIP engine
Summary
"logprobs": true(with"top_logprobs": 0..20) on/v1/chat/completionsnow returnschoices[0].logprobs.content, as OpenAI and llama-server do. Until now only the SYCL port had it (835d30b); the CUDA/HIP engine andserve/accepted the field and returned nothing. A client that marks values the model was unsure of (the weakest token of a value under 50%) needs it.What changed
src/program/generate.cpp): the GEN keylogprobs=KwritesLP <logprob> [id:logprob ...]after eachT <id>, from the verify window's head logits (Verifier::copy_logits), before penalties, temperature and sampling. It is the SYCL port's code, placed at both token outputs (the plain and the pipelined path). Without the key, no LP lines and nothing else changes.serve/server.py:sampling_keyssendslogprobs=Konly when the request asks; an engine without LP lines ignores the key and the reply has nologprobs.StrataEngine.generatereads a token's LP line before it yields the token (the engine writes both in the same flush), so the logprobs belong to the right text chunk. When nothing follows a token within 0.5 s, it is yielded without one. A line read this way is handed to the early-stop drain, so aDONEread after the last token is not waited for again (tested).tokenis the text the token completes (an incremental UTF-8 decode, as the reply's text is made): a character split across tokens is in the token that finishes it, so the tokens, joined, spell the reply.bytesare the token's own bytes. Tokens that make no text (</think>and the newlines after it) and the stop token are left out.openai_collectmerges them intochoices[0].logprobs(content,refusal, andreasoning_contentwith thinking on).response_formatjson_schema: withlogprobs, the answer is returned as the model wrote it (validated as before, the same value) when its tokens spell it, including the JSON inside a```jsonfence; otherwise the canonical JSON withoutlogprobs. Withoutlogprobs, the canonical JSON as before.docs/DETAILS.md: a "Logprobs" paragraph after the sampling one.serve/test_logprobs.py: the request keys, the engine line pairing (with and without LP lines, not asked, early stop), the API streamed and not (multi-byte characters,top_logprobs, the stop token), thinking,json_schemawith a fenced answer, and the span helper.Extra Notes
logprobssends the same GEN line, reads the same lines and returns the same reply (tested). The engine prints LP lines only when asked.--spec 4 --mtp), this branch built withtools/hip/build_windows.bat(gfx1151, engine 0.1.42):logprobs, 38.5 and 38.1 tok/s; 254 entries (the stop token left out).{"rent": "$1,250", "quote": "Rent is $1,250 a month."}from a two-fieldjson_schemaat temperature 0: 26 tokens that spell the content.contentandreasoning_contenteach spelled by their tokens. A streamedjson_objectanswer and a sentence withcafé,東京and€5: spelled too.--parallelbatch slots (theirBTlines get no LP lines). The legacy completions form"logprobs": Nis taken astop_logprobsN.servetest files pass onmain61b3fb5 (704 tests).🤖 Generated with Claude Code