feat: add Qwen3-8B template - #47
Conversation
| {%- else %} | ||
| {{- content }} |
There was a problem hiding this comment.
Is this the history content? If so, it is part of the training here, but I think we only want to train on the last generation and have the rest as context. So I think we want to exclude this part, maybe by doing sth like this:
{%- set assistant_body %}
…existing think/content logic, tool_calls loop, '<|im_end|>'…
{%- endset %}
{%- if loop.index0 > ns.last_query_index %}
{%- generation %}{{- assistant_body -}}{%- endgeneration %}
{%- else %}
{{- assistant_body }}
{%- endif %}There was a problem hiding this comment.
Thanks for pointing this out. Yes, the else block is the history content. I've fixed it using your using suggestion.
Qwen3 drops <think> from assistant turns at or before the last user query, so history rendered without a reasoning trace was being trained on.
Qwen3 drops <think> from assistant turns at or before the last user query, so history rendered without a reasoning trace was being trained on.
Vendors the pristine Qwen/Qwen3-8B chat_template as a fixture and diffs rendered output across the 20-combination matrix.
Done. Also added a test to ensure that messages rendered with the new template are byte-equivalent to those rendered with the upstream template.
Fair point. All Qwen3 models except |
Do we have a strong reason for this? The only explanation I can think of is that the non-final assistant turns wouldn't have the reasoning traces, so the model would be learning to produce a mix of reasoning as well as non-reasoning outputs. On the other hand, wouldn't we be losing a lot of training signal in case of multi-turn conversations? |
|
AFAIK, this is how the Qwen template handles it. |
|
Nice, this looks right to me now. I checked the rendering on the normal shapes and it does what it should. One thing worth handling before this goes in, and I think it should go into this PR. Gating the markers makes zero-span rows possible. If a conversation has no assistant turn after the last real user message, there is no generation region and the mask comes out empty. TRL checks for exactly that and raises inside
So one bad row kills tokenization for the whole mixture, before a single training step, with an error that points at the template instead of the data. It splits in two, and only half of it is about Qwen:
So the solution is part global, part template-aware:
Also worth a zero-span test – the current masking tests cover marker presence and history exclusion, which is why this didn't show up. One edge case for completeness, though I doubt it shows up in practice: a conversation ending on a What do you think? |
Summary
Adds
qwen3-8b.jinjato the chat-template registry directory: Qwen3-8B's upstream template with{% generation %}markers spliced in so it can driveassistant_only_loss=True.The upstream template concatenates the turn header and the message body into a single emission (
'<|im_start|>' + message.role + '\n' + content), so the markers can't simply wrap the assistant branch. The header emission is split out and left outside the markers;{% generation %}opens after it and closes around<|im_end|>, with the trailing\noutside. Result: assistant content, reasoning trace and tool calls are in the loss, prompt tokens and the turn separator are not.Type of change
Validation
Rendered output compared byte-for-byte against the pristine upstream template across 20 combinations — 5 conversation shapes (single-turn reasoning, multi-turn, no-think, tool call, system+tools) ×
add_generation_prompton/off × with/withouttools— all identical, confirming the restructuring is loss-mask-only and changes no inference behaviour.