Add max_thinking_tokens for reasoning models (issue #42111)#42112
Add max_thinking_tokens for reasoning models (issue #42111)#42112AndresAlgaba wants to merge 40 commits into
Conversation
Add feature described in Issue huggingface#42111
|
One thing that still bothers me is that: from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "Qwen/Qwen3-4B"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="auto", device_map="auto")
messages = [{"role": "user", "content": "Give me a concise proof that sqrt(2) is irrational."}]
inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to(model.device)
think_start = tokenizer.convert_tokens_to_ids("<think>")
think_end = tokenizer.convert_tokens_to_ids("</think>")
model.generation_config.begin_thinking_token_id = think_start
model.generation_config.end_thinking_token_id = think_end
out = model.generate(
**inputs,
max_new_tokens=128,
max_thinking_tokens=64,
do_sample=False,
)
print(tokenizer.decode(out[0], skip_special_tokens=False))will result in: <|im_start|>user
Give me a concise proof that sqrt(2) is irrational.<|im_end|>
<|im_start|>assistant
<think>
Okay, so I need to prove that the square root of 2 is irrational. Hmm, I remember that an irrational number is a number that can't be expressed as a fraction of two integers. So, I need to show that there are no integers a and b where a/b equals sqrt(2). Let</think>
To prove that √2 is irrational, we use a **proof by contradiction**. Here's a concise version:
1. **Assume** √2 is rational. Then it can be written as a fraction $ \frac{a}{b} $, where $ a $ and $ bwhere the <|im_start|>user
Give me a concise proof that sqrt(2) is irrational.<|im_end|>
<|im_start|>assistant
<think>
Okay, so I need to prove that the square root of 2 is irrational. Hmm, I remember that an irrational number is a number that can't be expressed as a fraction of two integers. So, I need to show that there are no integers a and b where a/b equals sqrt(2). Let
</think>
To prove that √2 is irrational, we use a **proof by contradiction**. Here's a concise version:
1. **Assume** √2 is rational. Then it can be written as a fraction $ \frac{a}{b} $, where $ a $ and $ b |
|
Additionally, the early stopping of the Qwen example (https://qwen.readthedocs.io/en/latest/getting_started/quickstart.html#thinking-budget) adds: Not sure whether such customization should be allowed? Kind of related to the comment above, but seems like a different logic and maybe for a future PR? |
|
I might have overcomplicated the |
|
Another issue is when |
|
While testing this, another problem is that the model might output a second from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "Qwen/Qwen3-4B"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="auto", device_map="auto")
messages = [
{
"role": "user",
"content": "Give me a concise proof that sqrt(2) is irrational.",
},
]
inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to(model.device)
think_start = tokenizer.convert_tokens_to_ids("<think>")
think_end = tokenizer.convert_tokens_to_ids("</think>")
model.generation_config.begin_thinking_token_id = think_start
model.generation_config.end_thinking_token_id = think_end
out = model.generate(
**inputs,
max_new_tokens=64,
max_thinking_tokens=32,
do_sample=False,
)
print(tokenizer.decode(out[0], skip_special_tokens=False))<|im_start|>user
Give me a concise proof that sqrt(2) is irrational.<|im_end|>
<|im_start|>assistant
<think>
Okay, so I need to prove that the square root of 2 is irrational. Hmm, I remember that an irrational number is a number that can't</think>
</think>
Sure! Here's a concise proof that $\sqrt{2}$ is irrational:
---
**Proof that $\sqrt{2}$ is irrational |
|
Update should satisfy following requirements:
|
|
|
||
| first_open_position: Optional[int] = None | ||
| if prompt_open_depth > 0: | ||
| first_open_position = max(prompt_length - 1, 0) |
There was a problem hiding this comment.
i wonder how often we get a prompt which started thinking but didn't finish yet 🤔
There was a problem hiding this comment.
Good question. It might be an edge case, but, for example, it happens when a token gets automatically appended when enable_thinking=True, or is a user (for no good reason) puts in their prompt. Or when the messages contain earlier assistant parts, which still contain the reasoning (without closing properly). However, I now only look back a few tokens in the prompt and have added the _prompt_prefilled_suffix_length argument.
There was a problem hiding this comment.
Hmm, I'd personally prefer to not add more args in the generation config. It's already quite bloated due to historical reasons and we've been trying to not add new generation techniques recently (instead re-directing them to hub as custom code)
There was a problem hiding this comment.
Ok, I see. In the most recent version I added this, but happy to change it!
|
@AndresAlgaba hey! |
|
I'm actually not sure about this PR! I think forcibly ending model thinking mid-stream will force them out of distribution and cause them to output very low-quality answers. We could add it as a feature for advanced users, but I worry that if we do that then people will assume that because it's a feature that it works well in practice, when I think it probably won't, right? Note that in the Qwen example, they don't cut off thinking directly, but instead insert an "early stopping prompt" that is likely fine-tuned for Qwen and will not work well on other thinking models. |
|
@zucchini-nlp Thank you for the feedback, working on it! @Rocketknight1 Thanks for the comment! Indeed, I agree it is more of an experimental feature. It is in line with Gemini's thinking_budget (https://ai.google.dev/gemini-api/docs/thinking#set-budget- and Claude's budget_tokens (https://docs.claude.com/en/docs/build-with-claude/extended-thinking#how-to-use-extended-thinking). I remember that DeepSeek was planning to implement a max_thinking_tokens parameter at some point, but I can't find it anymore. I implemented it for my own research and thought others might be interested in this feature, but with the warranty that it is experimental. So not sure whether it should then be added to the library like this? |
|
hi @AndresAlgaba, yes, but note that that feature in Gemini or Claude does not actually cap the reasoning tokens or forcibly stop thinking - they specifically say the model can overflow the budget. They've probably just trained the model so that it (roughly) obeys thinking budgets, and then they probably tell it the budget in the prompt somewhere |
|
True, same for the reasoning effort in OpenAI models. I still believe some people might be interested in the feature, see a similar feature request in vllm: vllm-project/vllm#17887 (and PR: vllm-project/vllm#20859, where they also propose a logit processor). Is there any way we can indicate it's experimental? Is it an option to keep the processor without adding the I believe that adding the |
|
Hmn, if we can mention in the docs that it's experimental and might reduce answer quality it's probably okay! cc @zucchini-nlp, are you happy with that? |
|
Thanks again for the feedback! I have made some changes after further experimentation and have updated the docs (including a warning that the feature is experimental). |
zucchini-nlp
left a comment
There was a problem hiding this comment.
@AndresAlgaba thanks for iterating on this!
My first review might have been a bit rushed. Looking at comments from @Rocketknight1 and how thinking budget is handled in some other models, truncating generation might not be the best option performance-wise. I realize though that the feature is requested a lot and community is excited to have smth working
So, to check community reaction and assess usage, wdyt of hosting it as a custom generation method on the hub (following the docs)? We can simply copy the logits processor to the hub and call model.generate() with it (I can help with that and add in transformers-community org). It will also give us more freedom to experiment and introduce breaking changes in the future
|
Seems like a good idea! I agree that hard truncation is often not the way to go. Thank you for offering help, I will look into it and keep you posted here! |
|
Hi @zucchini-nlp, sorry for not following up on this but I decided to look further into the "early stopping" schemes and write a paper on it (now available on arxiv: https://arxiv.org/abs/2601.23163). I will experiment further and then start to add to the |
|
Amazing @AndresAlgaba ! If you have a paper/blog-post, feel free to link it directly in the hub page via |
Add feature described in Issue #42111
What does this PR do?
A built-in way to cap how many tokens a reasoning model spends inside its
<think> … </think>block. Today, we can only control the total response length viamax_new_tokens. No parameter limits the internal reasoning segment whenenable_thinking=True.Motivation:
This PR:
GenerationConfigwith:max_thinking_tokens: integer budget for reasoning tokens.begin_thinking_token_id / end_thinking_token_id: marker IDs so generation knows where the thinking span begins/ends.MaxThinkingTokensLogitsProcessorthat watches the active<think>block. Once the budget is reached, it forces end_thinking_token_id, ensuring the model exits reasoning and continues with the final response._get_logits_processorinjects the new processor whenever the config is fully specified.Fixes #42111
Before submitting
Pull Request section?
to it if that's the case.
documentation guidelines, and
here are tips on formatting docstrings.
Who can review?
Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.
@ArthurZucker @Cyrilvallez @zucchini-nlp