Issues / #3354

#3354 [Engine] qwen35moe: the model-card sampling is applied silently and disables MTP drafting (drafts accepted 0 of 0)

open · @conlododo · 2 comentários · No GitHub

BenchmarksSetup & installServer & APINVIDIA / CUDAModels & quantsWindowsLinux

Descrição

### Area

[Engine]  (speed, kernels, models, the server and API)

### Desktop app, Terminal or source?

Source (START-HERE.bat, setup.sh or a build)

### What happened

With a Qwen3.6-35B-A3B (`qwen35moe`) model, `--mtp 2` loads the MTP block and the engine warms up with "a speculative step", but every served request reports `drafts accepted 0 of 0`, so decode stays at the no-drafting speed (73-88 tok/s where the same machine reaches ~150 tok/s with drafts). Sending the same request straight to the engine's own protocol (`GEN 64 <ids>` on its stdin, bypassing `serve/server.py`) produces drafts normally: `DONE 64 16 132.3 431.5 length 43 44` (43 of 44 accepted) and `DONE 256 16 117.8 1631.6 length 167 176` (167 of 176, ~157 tok/s).

The cause is that the server fills in the model card's sampling when a request names none, and the q36 engine only drafts for a strictly greedy request. In `serve/server.py`, `MODEL_CARD_SAMPLING["qwen35moe"]` is `{"thinking": {"temperature": 1.0, "top_p": 0.95, "top_k": 20, "presence_penalty": 1.5}, "instruct": {...}}`; in `src/qwen35/q36_engine.cu:3584` the door is `const bool spec = E.mtp_d > 0 && !smp.needs_logits() && !adm;` and `needs_logits() = temperature > 0 || repeat != 1 || freq != 0 || present != 0` (line 3013). A plain OpenAI request (`{"messages": [...], "max_tokens": 64}`, no sampling fields) therefore reaches the engine as `GEN 64 temperature=1.0 top_p=0.95 top_k=20 penalty_present=1.5 penalty_last_n=64 <ids>`, and `spec` is false for every window. Setups where MTP is the main reason to run this family lose the whole benefit without any error or warning.

### Smallest steps or command that reproduce it

```
# Strata 0.1.45 from source, Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf (753 tensors, 22 GB, 5.15 BPW), --max-context 81920 --mtp 2
# 1) a request that names no sampling, through the server:
curl -s http://127.0.0.1:8081/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"...","messages":[{"role":"user","content":"1+1=?"}],"max_tokens":64}'
#    engine log: drafts accepted 0 of 0, ~74 tok/s

# 2) the same shape, straight to the engine (engine/strata --serve ... on stdin):
GEN 64 1,2,3,4,5,6,7,8,9,10,11,12,13,14
#    DONE 64 16 132.3 431.5 length 43 44    (~150 tok/s)
```

To see the difference without reading the server sources, point the config's `exe` at a one-file pass-through proxy that logs stdin before forwarding; that is how the `GEN` line above was captured.

### Reproduction status

Reproduced locally

### Strata version

strata 0.1.45 (built from source; `engine/strata --version` reports `strata 0.1.45`)

### Graphics card, memory and system

RTX 5060 Ti 16 GB · 30 GB RAM · Ubuntu 24.04, Linux 7.0.0-38

### Model, size and settings

Qwen3.6-35B-A3B-UD-Q4_K_XL · UD-Q4_K_XL (Unsloth Dynamic, 22 GB, 753 tensors, 5.15 BPW) · context 81920 · --mtp 2 · --prefill auto. The GGUF is an upstream file with a LoRA merged back in; its tensor types were restored to the base file's per-tensor types and it loads normally.

### Logs

```
# straight to the engine (drafting on)
DONE 64 16 132.3 431.5 length 43 44 0 0 0 0 0 0.0 16 0
DONE 256 16 117.8 1631.6 length 167 176 0 0 0 0 0 0.0 16 0

# through serve/server.py, request with no sampling fields (drafting off)
GEN 64 temperature=1.0 top_p=0.95 top_k=20 penalty_present=1.5 penalty_last_n=64 248045,846,198,...
strata serve: session 0: prompt 14 tokens = 0 reused + 14 read in 174 ms (80.5 tok/s), 64 generated in 870 ms (73.6 tok/s), drafts accepted 0 of 0 (length)

# the engine's own view while the block is loaded
strata-q36: context 81920 cells, prompt chunk 2048, MTP block loaded, 4.81 GiB of VRAM free
strata-q36: warmed up in 0.9 s (one prompt chunk, a decode step, a speculative step)
```

### Workaround that works

Put a `sampling` block in the run config so it overrides the model card's set:

```json
"sampling": { "temperature": 0, "top_p": 1.0, "presence_penalty": 0 }
```

`top_k` must be left out (the config validation accepts 1..64 only). With that in place, drafts come back on the same machine and model: code-heavy 300-token output went from ~88 to 104 tok/s at `--mtp 2` (187/224 drafts accepted) and 113 tok/s at `--mtp 4` (221/320); a 256-token chat answer went from ~77 to 95 tok/s (62/128).

### What would help (one of these)

1. Say it: log one line when MTP is loaded but the resolved sampling rules out drafting (`drafts disabled: temperature 1.0`), instead of leaving the user to discover it from `0 of 0`.
2. Prefer drafting: when `--mtp N` is given, keep the request greedy unless the client asked for sampling itself, or document that MTP requires `temperature=0` with no penalties and that the model-card set turns it off.
3. A middle ground: a `--mtp-greedy` or config switch that drops the model-card defaults when MTP is on, keeping them for the plain path.

Happy to test a patch on a 16 GB and a 32 GB card and report the numbers.

No site

Links install, modelos, releases.