Repository navigation
feat: add Qwen3-8B template - #47
Conversation
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? |
Yes. This will do.
I actually ran into this problem too when I was testing out this template. I also observed the edge cases that you speak of (turn ending in tool use, no assistant turn after the last user turn). The solutions you propose make sense, but I think the most robust solution would be to simply apply the chat template and filter out rows that have have empty loss masks. Otherwise, we risk an edge case that we haven't considered crashing the run at a later point. Consider: What do you think? |
|
See #48 for the full script. |
|
I see the point. Using the template itself to filter is definitely the most robust solution. I'm just worried that this will drop unconditionally: If a dataset loses 5%, we don't really know what the issue was. Could we pair the mask check with a categorised count of the failure modes we already know: no assistant turn at all, no assistant turn after the last real user query, and an "other" bucket? The mask stays the authority on keep/drop, the categories only say why, and "other" being non-zero is the useful signal, since that's a shape none of us has thought about yet. This way we can catch irregularities. Moreover, I think we could implement this inside |
Yes, this makes sense. I've changed the implementation of the row filters to accommodate this. |
* fix: warn when max_length cuts a supervised span part-way `any(assistant_masks)` only catches rows whose assistant turn is truncated away entirely. A row that straddles max_length keeps a non-zero mask, so it stays in training and teaches an answer cut mid-sentence with no end-of-turn token. One untruncated render classifies all three cases instead — no assistant tokens, all of them past the cap, or straddling it — at the same cost. The keep/drop decision is unchanged. * feat: add sft.truncated_span_action to drop rows a cut span damages Warning is right when the cap is wrong, since raising max_seq_length keeps the data. It is not enough when the sequence length is fixed by memory and dropping is the only lever left, so make it a switch. Default stays warn. Drop still warns. data.datasets[].weight multiplies the rows that survive filtering, so an uneven drop changes a dataset's share of the mixture while the config still claims the original weights — that has to be in the log rather than inferred from a row count. --------- Co-authored-by: Konstantin Nikolaou <knikolaou@icp.uni-stuttgart.de>
KonstiNik
left a comment
There was a problem hiding this comment.
LGTM now. Happy to merge it.
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.