Skip to content

Commit 530716a

Browse files
uralikIlia Kulikov
andauthored
chat template overwriting, chat mode in instruction and preference recipes (#1228)
* chat template overwriting, chat mode in SFT dataset * qwen template added * formatting * dpo recipe with chat mode * extra newline fix in the template * introducing chat template interface * lint * lint * lint * removing v0.4 compatibility --------- Co-authored-by: Ilia Kulikov <kulikov@meta.com>
1 parent 3b33a73 commit 530716a

9 files changed

Lines changed: 302 additions & 87 deletions

File tree

src/fairseq2/assets/cards/models/qwen.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ use_im_end: true
6464

6565
---
6666

67-
name: qwen3_0.6b
67+
name: qwen3_0.6b_base
6868
model_family: qwen
6969
model_arch: qwen3_0.6b
7070
checkpoint: "hg://qwen/qwen3-0.6b-base"
@@ -73,7 +73,7 @@ tokenizer_family: qwen
7373

7474
---
7575

76-
name: qwen3_0.6b_instruct
76+
name: qwen3_0.6b
7777
model_family: qwen
7878
model_arch: qwen3_0.6b
7979
checkpoint: "hg://qwen/qwen3-0.6b"
@@ -83,7 +83,7 @@ use_im_end: true
8383

8484
---
8585

86-
name: qwen3_1.7b
86+
name: qwen3_1.7b_base
8787
model_family: qwen
8888
model_arch: qwen3_1.7b
8989
checkpoint: "hg://qwen/qwen3-1.7b-base"
@@ -92,7 +92,7 @@ tokenizer_family: qwen
9292

9393
---
9494

95-
name: qwen3_1.7b_instruct
95+
name: qwen3_1.7b
9696
model_family: qwen
9797
model_arch: qwen3_1.7b
9898
checkpoint: "hg://qwen/qwen3-1.7b"
@@ -102,7 +102,7 @@ use_im_end: true
102102

103103
---
104104

105-
name: qwen3_4b
105+
name: qwen3_4b_base
106106
model_family: qwen
107107
model_arch: qwen3_4b
108108
checkpoint: "hg://qwen/qwen3-4b-base"
@@ -111,7 +111,7 @@ tokenizer_family: qwen
111111

112112
---
113113

114-
name: qwen3_4b_instruct
114+
name: qwen3_4b
115115
model_family: qwen
116116
model_arch: qwen3_4b
117117
checkpoint: "hg://qwen/qwen3-4b"
@@ -121,7 +121,7 @@ use_im_end: true
121121

122122
---
123123

124-
name: qwen3_8b
124+
name: qwen3_8b_base
125125
model_family: qwen
126126
model_arch: qwen3_8b
127127
checkpoint: "hg://qwen/qwen3-8b-base"
@@ -130,7 +130,7 @@ tokenizer_family: qwen
130130

131131
---
132132

133-
name: qwen3_8b_instruct
133+
name: qwen3_8b
134134
model_family: qwen
135135
model_arch: qwen3_8b
136136
checkpoint: "hg://qwen/qwen3-8b"
@@ -140,7 +140,7 @@ use_im_end: true
140140

141141
---
142142

143-
name: qwen3_14b
143+
name: qwen3_14b_base
144144
model_family: qwen
145145
model_arch: qwen3_14b
146146
checkpoint: "hg://qwen/qwen3-14b-base"
@@ -149,7 +149,7 @@ tokenizer_family: qwen
149149

150150
---
151151

152-
name: qwen3_14b_instruct
152+
name: qwen3_14b
153153
model_family: qwen
154154
model_arch: qwen3_14b
155155
checkpoint: "hg://qwen/qwen3-14b"
@@ -159,7 +159,7 @@ use_im_end: true
159159

160160
---
161161

162-
name: qwen3_32b_instruct
162+
name: qwen3_32b
163163
model_family: qwen
164164
model_arch: qwen3_32b
165165
checkpoint: "hg://qwen/qwen3-32b"

src/fairseq2/data/text/tokenizers/hg.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from collections.abc import Sequence
1010
from pathlib import Path
11-
from typing import TYPE_CHECKING, final
11+
from typing import TYPE_CHECKING, Any, final
1212

1313
import torch
1414
from torch import Tensor
@@ -19,6 +19,9 @@
1919
AutoTokenizer,
2020
PreTrainedTokenizer,
2121
)
22+
from transformers.tokenization_utils_base import ( # type: ignore[import-not-found, attr-defined]
23+
BatchEncoding,
24+
)
2225
except ImportError:
2326
_has_hg_transformers = False
2427
else:
@@ -44,8 +47,12 @@ def load_hg_token_model(
4447

4548
@final
4649
class HuggingFaceTokenModel:
50+
_tok: PreTrainedTokenizer
51+
4752
def encode(self, text: str) -> list[int]: ...
4853

54+
def overwrite_chat_template(self, chat_template: str) -> None: ...
55+
4956
def decode(
5057
self, token_indices: list[int], skip_special_tokens: bool = False
5158
) -> str: ...
@@ -111,6 +118,9 @@ def __init__(
111118
def encode(self, text: str) -> list[int]:
112119
return self._tok.encode(text, add_special_tokens=False)
113120

121+
def overwrite_chat_template(self, chat_template: str) -> None:
122+
self._tok.chat_template = chat_template
123+
114124
def decode(
115125
self, token_indices: list[int], skip_special_tokens: bool = False
116126
) -> str:
@@ -207,6 +217,11 @@ def encode_as_tokens(self, text: str) -> list[str]:
207217

208218
return self._model.convert_ids_to_tokens(indices)
209219

220+
def apply_chat_template(self, chat: Any, **kwargs: Any) -> Any | BatchEncoding:
221+
# pass all kwargs to HG tokenizer as is here
222+
encoded_output = self._model._tok.apply_chat_template(chat, **kwargs)
223+
return encoded_output
224+
210225
@property
211226
@override
212227
def prefix_indices(self) -> Tensor | None:

src/fairseq2/data/text/tokenizers/llama.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@
3737
)
3838
from fairseq2.device import Device
3939

40+
# llama3 chat template with assistant mask support, see https://github.com/huggingface/transformers/issues/28950
41+
LLAMA3_CHAT_TEMPLATE = """{{- bos_token }}
42+
{%- for message in messages %}
43+
{%- if message.role == 'user' or message.role == 'system' %}
44+
{{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n' + message['content'] | trim + '<|eot_id|>' }}
45+
{%- else %}
46+
{%- generation %}
47+
{{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' + message['content'] | trim + '<|eot_id|>' }}
48+
{%- endgeneration %}
49+
{%- endif %}
50+
{%- endfor %}
51+
{%- if add_generation_prompt %}
52+
{{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}
53+
{%- endif %}"""
54+
4055

4156
@final
4257
class LLaMA3Tokenizer(TextTokenizer):
@@ -291,6 +306,7 @@ def load_llama3_hg_tokenizer(path: Path, card: AssetCard) -> TextTokenizer:
291306
boh_token="<|start_header_id|>",
292307
eoh_token="<|end_header_id|>",
293308
)
309+
model.overwrite_chat_template(LLAMA3_CHAT_TEMPLATE)
294310
except (OSError, RuntimeError) as ex:
295311
raise TextTokenizerLoadError(
296312
card.name, f"The '{card.name}' text tokenizer model cannot be loaded. See the nested exception for details." # fmt: skip

src/fairseq2/data/text/tokenizers/qwen.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,71 @@
2828
)
2929
from fairseq2.device import Device
3030

31+
# qwen chat template with assistant mask support. ALWAYS HAS THINKING TOKENS HERE
32+
QWEN_CHAT_TEMPLATE = """{%- if tools %}
33+
{{- '<|im_start|>system\\n' }}
34+
{%- if messages[0].role == 'system' %}
35+
{{- messages[0].content + '\\n\\n' }}
36+
{%- endif %}
37+
{{- "# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>" }}
38+
{%- for tool in tools %}
39+
{{- "\\n" }}
40+
{{- tool | tojson }}
41+
{%- endfor %}
42+
{{- '\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\\n</tool_call><|im_end|>\\n' }}
43+
{%- else %}
44+
{%- if messages[0].role == 'system' %}
45+
{{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}
46+
{%- endif %}
47+
{%- endif %}
48+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
49+
{%- for message in messages[::-1] %}
50+
{%- set index = (messages|length - 1) - loop.index0 %}
51+
{%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
52+
{%- set ns.multi_step_tool = false %}
53+
{%- set ns.last_query_index = index %}
54+
{%- endif %}
55+
{%- endfor %}
56+
{%- for message in messages %}
57+
{%- if message.content is string %}
58+
{%- set content = message.content %}
59+
{%- else %}
60+
{%- set content = '' %}
61+
{%- endif %}
62+
{%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
63+
{{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}
64+
{%- elif message.role == "assistant" %}
65+
{%- set reasoning_content = '' %}
66+
{%- if message.reasoning_content is string %}
67+
{%- set reasoning_content = message.reasoning_content %}
68+
{%- else %}
69+
{%- if '</think>' in content %}
70+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}
71+
{%- set content = content.split('</think>')[-1].lstrip('\\n') %}
72+
{%- endif %}
73+
{%- endif %}
74+
{{- '<|im_start|>' + message.role }}
75+
{%- generation %}
76+
{%- if loop.index0 > ns.last_query_index %}
77+
{%- if loop.last or (not loop.last and reasoning_content) %}
78+
{{- '<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}
79+
{%- else %}
80+
{{- content }}
81+
{%- endif %}
82+
{%- else %}
83+
{{- content }}
84+
{%- endif %}
85+
{{- '<|im_end|>' }}
86+
{%- endgeneration %}
87+
{%- endif %}
88+
{%- endfor %}
89+
{%- if add_generation_prompt %}
90+
{{- '<|im_start|>assistant\\n' }}
91+
{%- if enable_thinking is defined and enable_thinking is false %}
92+
{{- '<think>\\n\\n</think>\\n\\n' }}
93+
{%- endif %}
94+
{%- endif %}"""
95+
3196

3297
@final
3398
class QwenTokenizer(TextTokenizer):
@@ -123,6 +188,7 @@ def load_qwen_tokenizer(path: Path, card: AssetCard) -> TextTokenizer:
123188
boh_token=None,
124189
eoh_token=None,
125190
)
191+
model.overwrite_chat_template(QWEN_CHAT_TEMPLATE)
126192
except (OSError, RuntimeError) as ex:
127193
raise TextTokenizerLoadError(
128194
card.name, f"The '{card.name}' text tokenizer model cannot be loaded. See the nested exception for details." # fmt: skip

src/fairseq2/datasets/instruction.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
read_sequence,
2727
)
2828
from fairseq2.data.text.tokenizers import TextTokenizer
29+
from fairseq2.data.text.tokenizers.hg import HuggingFaceTokenEncoder
2930
from fairseq2.datasets import (
3031
DataPipelineReader,
3132
DataReader,
@@ -56,6 +57,8 @@ class InstructionReadOptions(DataReadOptions):
5657
target_encode_mode: str = "prompt_response"
5758
"""The tokenizer mode to encode the target text."""
5859

60+
chat_mode: bool = False
61+
5962

6063
@dataclass
6164
class InstructionPromptReadOptions(DataReadOptions):
@@ -230,26 +233,55 @@ def create_reader(
230233

231234
seed += gang.rank
232235

233-
# Encode source and target texts.
234-
source_encoder = tokenizer.create_encoder(mode=options.source_encode_mode)
235-
target_encoder = tokenizer.create_encoder(mode=options.target_encode_mode)
236+
if options.chat_mode is True:
237+
# not passing any encoding modes here, because we use apply_chat_template here
238+
encoder = tokenizer.create_encoder()
239+
if not isinstance(encoder, HuggingFaceTokenEncoder):
240+
raise RuntimeError(
241+
"Huggingface tokenizer must be used when chat_mode is True"
242+
)
243+
else:
236244

237-
builder.map(source_encoder, selector="src")
238-
builder.map(target_encoder, selector="tgt")
245+
def encoding_chat(example: dict[str, Any]) -> dict[str, Any]:
246+
id_ = example.get("id")
247+
chat = example.get("chat")
239248

240-
def cat_source_and_target(example: dict[str, Any]) -> dict[str, Any]:
241-
id_ = example.get("id")
249+
encoded_output = encoder.apply_chat_template(
250+
chat,
251+
return_dict=True,
252+
return_assistant_tokens_mask=True,
253+
return_tensors="pt",
254+
)
242255

243-
source_indices = example["src"]
244-
target_indices = example["tgt"]
256+
indices = encoded_output["input_ids"][0]
257+
target_mask = encoded_output["assistant_masks"][0].bool()
258+
259+
return {"id": id_, "indices": indices, "target_mask": target_mask}
260+
261+
builder.map(encoding_chat)
262+
263+
else:
245264

246-
indices = torch.cat([source_indices, target_indices])
265+
# Encode source and target texts.
266+
source_encoder = tokenizer.create_encoder(mode=options.source_encode_mode)
267+
target_encoder = tokenizer.create_encoder(mode=options.target_encode_mode)
247268

248-
target_mask = torch.arange(len(indices)) >= len(source_indices)
269+
builder.map(source_encoder, selector="src")
270+
builder.map(target_encoder, selector="tgt")
249271

250-
return {"id": id_, "indices": indices, "target_mask": target_mask}
272+
def cat_source_and_target(example: dict[str, Any]) -> dict[str, Any]:
273+
id_ = example.get("id")
251274

252-
builder.map(cat_source_and_target)
275+
source_indices = example["src"]
276+
target_indices = example["tgt"]
277+
278+
indices = torch.cat([source_indices, target_indices])
279+
280+
target_mask = torch.arange(len(indices)) >= len(source_indices)
281+
282+
return {"id": id_, "indices": indices, "target_mask": target_mask}
283+
284+
builder.map(cat_source_and_target)
253285

254286
batching = options.batching
255287

@@ -312,11 +344,7 @@ def to_batch(example: dict[str, Any]) -> SequenceBatch:
312344

313345
seqs, seq_lens = indices["seqs"], indices["seq_lens"]
314346

315-
target_mask = example["target_mask"]["seqs"]
316-
317-
return SequenceBatch(
318-
seqs, seq_lens, target_mask=target_mask, example=example
319-
)
347+
return SequenceBatch(seqs, seq_lens, example=example)
320348

321349
pipeline = builder.map(to_batch).and_return()
322350

0 commit comments

Comments
 (0)