cloudyu commited on
Commit
ea13f62
·
verified ·
1 Parent(s): 9a468c7

serve_decide.py: answer read-out that works with speculative decoding (+ fallback), system2_only; serve.sh: --max-logprobs 256, MTP=1 option

Browse files
Files changed (3) hide show
  1. README.md +9 -8
  2. serve.sh +9 -3
  3. serve_decide.py +36 -4
README.md CHANGED
@@ -172,7 +172,8 @@ Lower `threshold` or `think_budget` to trade accuracy for speed; `thinking: "off
172
 
173
  MMLU-Pro and BBH (except six questions) ran with speculative decoding, which does not change the output distribution.
174
  Six BBH questions with 18 options hit a vLLM error in the speculative-decoding read-out path and were answered by the same
175
- server without speculative decoding.
 
176
 
177
  ## Images
178
 
@@ -289,13 +290,13 @@ requests.post("http://localhost:8000/v1/chat/completions", json={
289
  * Adaptive thinking adds nothing when it does not think, and about 1 second per 250 thinking tokens when it does
290
  (see [Latency](#latency)).
291
 
292
- **Faster thinking with speculative decoding.** Adding
293
- `--speculative-config '{"model": "google/gemma-4-26B-A4B-it-assistant", "num_speculative_tokens": 4}'` to `serve.sh`
294
- (Google's 0.9 GB draft model for this backbone) speeds up System 2 about 1.8–1.9× on one B200: 247 → 438 tokens per
295
- second for a single request, 7,072 → 13,505 tokens per second at 128 concurrent requests (mean acceptance length 3.5–3.7
296
- of 4). System 1 decisions are unchanged, but System 1 throughput at high concurrency drops (257 → 140 decisions per second
297
- with 64 clients), and in this vLLM build the System 2 answer read-out fails for questions with more than 16 options. Use it
298
- when you mostly think, and serve System 1-heavy traffic without it.
299
 
300
  If you call `/v1/completions` for System 1 yourself, pass `top_k: 0` and `top_p: 1.0`: the model's generation config sets
301
  `top_k=64` and `top_p=0.95`, which vLLM applies as request defaults and which would truncate the returned probabilities.
 
172
 
173
  MMLU-Pro and BBH (except six questions) ran with speculative decoding, which does not change the output distribution.
174
  Six BBH questions with 18 options hit a vLLM error in the speculative-decoding read-out path and were answered by the same
175
+ server without speculative decoding; the current `serve_decide.py` reads answers through a path that works with
176
+ speculative decoding.
177
 
178
  ## Images
179
 
 
290
  * Adaptive thinking adds nothing when it does not think, and about 1 second per 250 thinking tokens when it does
291
  (see [Latency](#latency)).
292
 
293
+ **Faster thinking with speculative decoding.** `MTP=1 bash GEV-26B-Decide/serve.sh` adds Google's 0.9 GB draft model for
294
+ this backbone (`--speculative-config '{"model": "google/gemma-4-26B-A4B-it-assistant", "num_speculative_tokens": 4}'`).
295
+ On one B200 it speeds up System 2 about 1.8–1.9×: 247 → 438 tokens per second for a single request, 7,072 → 13,505 tokens
296
+ per second at 128 concurrent requests (mean acceptance length 3.5–3.7 of 4). System 1 decisions are unchanged, but System 1
297
+ throughput at high concurrency drops (257 → 140 decisions per second with 64 clients); use it when you mostly think.
298
+ `serve_decide.py` reads answers with the token restriction that works under speculative decoding, which needs
299
+ `--max-logprobs 256` (set in `serve.sh`).
300
 
301
  If you call `/v1/completions` for System 1 yourself, pass `top_k: 0` and `top_p: 1.0`: the model's generation config sets
302
  `top_k=64` and `top_p=0.95`, which vLLM applies as request defaults and which would truncate the returned probabilities.
serve.sh CHANGED
@@ -1,16 +1,22 @@
1
  #!/usr/bin/env bash
2
  # Serve autotrust/GEV-26B-Decide with vLLM: one engine, both systems, text and images.
3
  # System 2: the unmodified gemma-4-26B-A4B-it (served name autotrust/GEV-26B-Decide) on the OpenAI endpoints
4
- # System 1: POST /v1/decide (2-256 options, optional adaptive thinking), or the LoRA module "jev-decision" directly
5
  # serve_decide.py is the standard vLLM OpenAI server (same flags) with /v1/decide added.
6
  # Requirements: a vLLM build with Gemma-4 support plus patches/vllm-gemma4-lm-head-lora.patch (LoRA on Gemma-4's tied
7
  # lm_head, vocabulary 262,144), tested with a vLLM development build from September 2026.
8
  # MAX_MODEL_LEN: up to 262144 (the backbone's native context).
 
 
9
  set -e
10
  MODEL_DIR=${MODEL_DIR:-GEV-26B-Decide}
11
  [ -d "$MODEL_DIR" ] || hf download autotrust/GEV-26B-Decide --local-dir "$MODEL_DIR"
 
 
 
 
12
  exec python3 "$MODEL_DIR/serve_decide.py" --model "$MODEL_DIR" --served-model-name autotrust/GEV-26B-Decide \
13
  --enable-lora --max-lora-rank 32 --lora-modules jev-decision="$MODEL_DIR/adapter_vllm" \
14
- --logprobs-mode processed_logprobs --max-model-len ${MAX_MODEL_LEN:-65536} --enable-prefix-caching \
15
  --max-num-seqs 256 --trust-request-chat-template --limit-mm-per-prompt '{"image": 8}' \
16
- --port ${PORT:-8000}
 
1
  #!/usr/bin/env bash
2
  # Serve autotrust/GEV-26B-Decide with vLLM: one engine, both systems, text and images.
3
  # System 2: the unmodified gemma-4-26B-A4B-it (served name autotrust/GEV-26B-Decide) on the OpenAI endpoints
4
+ # System 1: POST /v1/decide (2-256 options, adaptive thinking), or the LoRA module "jev-decision" directly
5
  # serve_decide.py is the standard vLLM OpenAI server (same flags) with /v1/decide added.
6
  # Requirements: a vLLM build with Gemma-4 support plus patches/vllm-gemma4-lm-head-lora.patch (LoRA on Gemma-4's tied
7
  # lm_head, vocabulary 262,144), tested with a vLLM development build from September 2026.
8
  # MAX_MODEL_LEN: up to 262144 (the backbone's native context).
9
+ # MTP=1: speculative decoding with google/gemma-4-26B-A4B-it-assistant (System 2 about 1.8-1.9x faster; lower System 1
10
+ # throughput at high concurrency).
11
  set -e
12
  MODEL_DIR=${MODEL_DIR:-GEV-26B-Decide}
13
  [ -d "$MODEL_DIR" ] || hf download autotrust/GEV-26B-Decide --local-dir "$MODEL_DIR"
14
+ EXTRA=()
15
+ if [ "${MTP:-0}" = "1" ]; then
16
+ EXTRA=(--speculative-config '{"model": "google/gemma-4-26B-A4B-it-assistant", "num_speculative_tokens": 4}')
17
+ fi
18
  exec python3 "$MODEL_DIR/serve_decide.py" --model "$MODEL_DIR" --served-model-name autotrust/GEV-26B-Decide \
19
  --enable-lora --max-lora-rank 32 --lora-modules jev-decision="$MODEL_DIR/adapter_vllm" \
20
+ --logprobs-mode processed_logprobs --max-logprobs 256 --max-model-len ${MAX_MODEL_LEN:-65536} --enable-prefix-caching \
21
  --max-num-seqs 256 --trust-request-chat-template --limit-mm-per-prompt '{"image": 8}' \
22
+ "${EXTRA[@]}" --port ${PORT:-8000}
serve_decide.py CHANGED
@@ -86,6 +86,7 @@ class DecideRequest(BaseModel):
86
  chat_template_kwargs: dict | None = None
87
  return_reasoning: bool = False
88
  debug: bool = False
 
89
 
90
 
91
  # ------------------------------------------------------------------ setup
@@ -137,6 +138,7 @@ def setup(args):
137
  "{%- endif -%}{%- endfor -%}{%- endif -%}{%- endfor -%}")
138
  names = args.served_model_name
139
  S.update(tok=tok, has_effort="reasoning_effort" in (tok.chat_template or ""), think_cache={},
 
140
  head=head, temps=temps, labels=[l for l, _ in dec], label_ids=[t for _, t in dec], ans_labels=ans, prof=prof,
141
  think_pre=pre, think_post=post, raw=raw, end_think_id=tok.convert_tokens_to_ids(prof["end_think"]),
142
  model=(names[0] if isinstance(names, list) else names) or args.model, model_type=mtype)
@@ -194,11 +196,32 @@ class Ctx:
194
  self.completion_tokens += out.usage.completion_tokens or 0
195
 
196
 
 
 
 
 
 
 
 
197
  async def _logprobs(ctx, content, has_img, ids, model, allowed=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  """One-token read-out: {token_id: logprob} for `ids` after the content (text or chat parts)."""
199
  lp = {}
200
- for i in range(0, len(ids), CHUNK):
201
- chunk = ids[i: i + CHUNK]
 
202
  kw = dict(logprob_token_ids=chunk) if allowed is None else dict(allowed_token_ids=allowed)
203
  if has_img:
204
  r = ChatCompletionRequest(model=model, messages=[{"role": "user", "content": content}], chat_template=S["raw"],
@@ -228,7 +251,7 @@ async def s1_pass(ctx, kind, parts, has_img, question, opts):
228
  ids = head["verbalizer_ids"][lo:hi]; bias = head["bias"][lo:hi]; lines = opts
229
  content = ([{"type": "text", "text": f"{prof['prefix']}[kind] {kind}\n[state] "}] + parts +
230
  [{"type": "text", "text": f"\n[question] {question}\n[options]\n" + "\n".join(lines) + "\n[decision]:"}])
231
- lp = await _logprobs(ctx, content, has_img, ids, LORA, allowed=ids if len(ids) <= 16 else None)
232
  return _softmax([(max(lp.get(t, -1e9), -1e9) + b) / temps[kind] for t, b in zip(ids, bias)])
233
 
234
 
@@ -302,7 +325,7 @@ async def s2_dist(ctx, kind, parts, has_img, question, opts, budget, want_text,
302
  ntok = out.usage.completion_tokens if out.usage else None
303
  read = user + [{"type": "text", "text": thought + S["prof"]["end_think"] + S["prof"]["after_think"] + "Answer: ("}]
304
  ids = [t for _, t in labs]
305
- lp = await _logprobs(ctx, read, True, ids, S["model"], allowed=ids if len(ids) <= 16 else None)
306
  p = _softmax([max(lp.get(t, -1e9), -1e9) for t in ids])
307
  if kind == "noul":
308
  p = [p[1], p[0]] # (A) yes / (B) no -> [P(false), P(true)]
@@ -356,6 +379,15 @@ async def decide(req: DecideRequest, raw: Request):
356
  parts, has_img = _parts(req.state)
357
  ctx = Ctx(raw)
358
  try:
 
 
 
 
 
 
 
 
 
359
  p1 = await s1_dist(ctx, req.kind, parts, has_img, req.question, opts, strategy)
360
  probs, think = p1, None
361
  if thinking == "on" or (thinking == "auto" and max(p1) < tau):
 
86
  chat_template_kwargs: dict | None = None
87
  return_reasoning: bool = False
88
  debug: bool = False
89
+ system2_only: bool = False # skip System 1: think and return System 2's answer distribution (for callers that already have p1)
90
 
91
 
92
  # ------------------------------------------------------------------ setup
 
138
  "{%- endif -%}{%- endfor -%}{%- endif -%}{%- endfor -%}")
139
  names = args.served_model_name
140
  S.update(tok=tok, has_effort="reasoning_effort" in (tok.chat_template or ""), think_cache={},
141
+ max_logprobs=int(getattr(args, "max_logprobs", 20) or 20),
142
  head=head, temps=temps, labels=[l for l, _ in dec], label_ids=[t for _, t in dec], ans_labels=ans, prof=prof,
143
  think_pre=pre, think_post=post, raw=raw, end_think_id=tok.convert_tokens_to_ids(prof["end_think"]),
144
  model=(names[0] if isinstance(names, list) else names) or args.model, model_type=mtype)
 
196
  self.completion_tokens += out.usage.completion_tokens or 0
197
 
198
 
199
+ def _readout_allowed(ids):
200
+ """Read-out path: restrict the next token to `ids` and return all of them as top logprobs when the server allows that
201
+ many (--max-logprobs); otherwise ask for `ids` explicitly with logprob_token_ids (chunks of 128). The first path also
202
+ works with speculative decoding; logprob_token_ids does not in this vLLM build. Both give the same distribution."""
203
+ return ids if len(ids) <= S["max_logprobs"] else None
204
+
205
+
206
  async def _logprobs(ctx, content, has_img, ids, model, allowed=None):
207
+ """One-token read-out with a fallback to the other read-out path if the first one fails."""
208
+ try:
209
+ return await _logprobs_once(ctx, content, has_img, ids, model, allowed)
210
+ except Upstream:
211
+ raise
212
+ except Exception:
213
+ other = None if allowed is not None else (ids if len(ids) <= S["max_logprobs"] else None)
214
+ if other is None and allowed is None:
215
+ raise
216
+ return await _logprobs_once(ctx, content, has_img, ids, model, other)
217
+
218
+
219
+ async def _logprobs_once(ctx, content, has_img, ids, model, allowed=None):
220
  """One-token read-out: {token_id: logprob} for `ids` after the content (text or chat parts)."""
221
  lp = {}
222
+ step = CHUNK if allowed is None else len(ids)
223
+ for i in range(0, len(ids), step):
224
+ chunk = ids[i: i + step]
225
  kw = dict(logprob_token_ids=chunk) if allowed is None else dict(allowed_token_ids=allowed)
226
  if has_img:
227
  r = ChatCompletionRequest(model=model, messages=[{"role": "user", "content": content}], chat_template=S["raw"],
 
251
  ids = head["verbalizer_ids"][lo:hi]; bias = head["bias"][lo:hi]; lines = opts
252
  content = ([{"type": "text", "text": f"{prof['prefix']}[kind] {kind}\n[state] "}] + parts +
253
  [{"type": "text", "text": f"\n[question] {question}\n[options]\n" + "\n".join(lines) + "\n[decision]:"}])
254
+ lp = await _logprobs(ctx, content, has_img, ids, LORA, allowed=_readout_allowed(ids))
255
  return _softmax([(max(lp.get(t, -1e9), -1e9) + b) / temps[kind] for t, b in zip(ids, bias)])
256
 
257
 
 
325
  ntok = out.usage.completion_tokens if out.usage else None
326
  read = user + [{"type": "text", "text": thought + S["prof"]["end_think"] + S["prof"]["after_think"] + "Answer: ("}]
327
  ids = [t for _, t in labs]
328
+ lp = await _logprobs(ctx, read, True, ids, S["model"], allowed=_readout_allowed(ids))
329
  p = _softmax([max(lp.get(t, -1e9), -1e9) for t in ids])
330
  if kind == "noul":
331
  p = [p[1], p[0]] # (A) yes / (B) no -> [P(false), P(true)]
 
379
  parts, has_img = _parts(req.state)
380
  ctx = Ctx(raw)
381
  try:
382
+ if req.system2_only:
383
+ if req.kind == "score":
384
+ return _err("system2_only is supported for noul and choice")
385
+ p2, think = await s2_dist(ctx, req.kind, parts, has_img, req.question, opts, req.think_budget, req.return_reasoning, tmpl)
386
+ k = max(range(len(p2)), key=p2.__getitem__)
387
+ return {"kind": req.kind, "options": opts, "probabilities": p2, "choice_index": k, "choice": opts[k], "system": 2,
388
+ "model": S["model"], "thinking": {"used": True, "budget": req.think_budget, **think},
389
+ "usage": {"prompt_tokens": ctx.prompt_tokens, "completion_tokens": ctx.completion_tokens},
390
+ "elapsed_seconds": time.time() - t0, "num_model_requests": ctx.requests}
391
  p1 = await s1_dist(ctx, req.kind, parts, has_img, req.question, opts, strategy)
392
  probs, think = p1, None
393
  if thinking == "on" or (thinking == "auto" and max(p1) < tau):