Repository navigation
Add DeepSeek-V4 chat encoding to SFT preprocessing #5883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |||||||||||||||||||||||||||||||||||||||||
| """Operations used by Grain""" | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import dataclasses | ||||||||||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||||||||||
| import warnings | ||||||||||||||||||||||||||||||||||||||||||
| from threading import current_thread | ||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Iterable, TYPE_CHECKING | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,6 +30,7 @@ | |||||||||||||||||||||||||||||||||||||||||
| from grain.experimental import TFRecordIterDataset | ||||||||||||||||||||||||||||||||||||||||||
| from maxtext.diffusion.block_diffusion import corruption as block_diffusion_corruption | ||||||||||||||||||||||||||||||||||||||||||
| from maxtext.input_pipeline.protos import example_pb2 | ||||||||||||||||||||||||||||||||||||||||||
| from maxtext.input_pipeline import encoding_dsv4 | ||||||||||||||||||||||||||||||||||||||||||
| from maxtext.input_pipeline import tokenizer | ||||||||||||||||||||||||||||||||||||||||||
| from maxtext.multimodal import processor as mm_processor | ||||||||||||||||||||||||||||||||||||||||||
| from maxtext.multimodal import utils as mm_utils | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -298,7 +300,89 @@ def _get_completion_in_chat_template(tokenizer_model, round_msgs): | |||||||||||||||||||||||||||||||||||||||||
| return completion_str | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def apply_chat_template(example, tokenizer_model, data_column_name): | ||||||||||||||||||||||||||||||||||||||||||
| _DS4_TURN_RE = re.compile( | ||||||||||||||||||||||||||||||||||||||||||
| re.escape(encoding_dsv4.ASSISTANT_SP_TOKEN) | ||||||||||||||||||||||||||||||||||||||||||
| + f"({re.escape(encoding_dsv4.thinking_start_token)}|" | ||||||||||||||||||||||||||||||||||||||||||
| + f"{re.escape(encoding_dsv4.thinking_end_token)})" | ||||||||||||||||||||||||||||||||||||||||||
| + f"(?:{re.escape(encoding_dsv4.DS_TASK_SP_TOKENS['action'])})?" | ||||||||||||||||||||||||||||||||||||||||||
| + f"(.*?{re.escape(encoding_dsv4.eos_token)})", | ||||||||||||||||||||||||||||||||||||||||||
| re.DOTALL, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def _is_deepseek_v4(tokenizer_model, model_name=None) -> bool: | ||||||||||||||||||||||||||||||||||||||||||
| """Detects DeepSeek-V4 by model or tokenizer name. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Vocabulary checks are not used: the DeepSeek-V3.2 tokenizer has the same special tokens and | ||||||||||||||||||||||||||||||||||||||||||
| also ships without a chat template, but uses a different encoding. | ||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||
| for name in (model_name, getattr(tokenizer_model, "model_name", None), getattr(tokenizer_model, "name_or_path", None)): | ||||||||||||||||||||||||||||||||||||||||||
| if name and any(x in str(name).lower() for x in ("deepseek4", "deepseek-v4", "deepseek_v4")): | ||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def _format_conversations_dsv4(example, data_column_name): | ||||||||||||||||||||||||||||||||||||||||||
| """Formats DeepSeek-V4 conversational data using encoding_dsv4.encode_messages.""" | ||||||||||||||||||||||||||||||||||||||||||
| raw_messages = example[data_column_name] | ||||||||||||||||||||||||||||||||||||||||||
| for idx, message in enumerate(raw_messages): | ||||||||||||||||||||||||||||||||||||||||||
| if message.get("role") == "system" and idx != 0: | ||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"System message found at index {idx}. System messages must be at index 0.") | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| assistants = [m for m in raw_messages if m.get("role") == "assistant"] | ||||||||||||||||||||||||||||||||||||||||||
| if not assistants: | ||||||||||||||||||||||||||||||||||||||||||
| example["is_prompt"] = [] | ||||||||||||||||||||||||||||||||||||||||||
| example[data_column_name] = [] | ||||||||||||||||||||||||||||||||||||||||||
| return example | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| last_assistant_idx = max(i for i, m in enumerate(raw_messages) if m.get("role") == "assistant") | ||||||||||||||||||||||||||||||||||||||||||
| messages_to_encode = list(raw_messages[: last_assistant_idx + 1]) | ||||||||||||||||||||||||||||||||||||||||||
| if example.get("tools") and not any(m.get("tools") for m in messages_to_encode): | ||||||||||||||||||||||||||||||||||||||||||
| target_idx = next( | ||||||||||||||||||||||||||||||||||||||||||
| (i for i, m in enumerate(messages_to_encode) if m.get("role") in ("system", "developer")), | ||||||||||||||||||||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| if target_idx is not None: | ||||||||||||||||||||||||||||||||||||||||||
| messages_to_encode[target_idx] = {**messages_to_encode[target_idx], "tools": example["tools"]} | ||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||
| messages_to_encode.insert(0, {"role": "system", "content": "", "tools": example["tools"]}) | ||||||||||||||||||||||||||||||||||||||||||
| thinking_mode = "thinking" if any(m.get("reasoning_content") for m in messages_to_encode) else "chat" | ||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For multi-turn thinking data,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not intended: those turns were trained without the reasoning that produced them. In thinking mode, |
||||||||||||||||||||||||||||||||||||||||||
| has_tools = bool(example.get("tools")) or any( | ||||||||||||||||||||||||||||||||||||||||||
| m.get("tools") or m.get("tool_calls") or m.get("role") == "tool" for m in messages_to_encode | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| text = encoding_dsv4.encode_messages(messages_to_encode, thinking_mode=thinking_mode, drop_thinking=not has_tools) | ||||||||||||||||||||||||||||||||||||||||||
| matches = list(_DS4_TURN_RE.finditer(text)) | ||||||||||||||||||||||||||||||||||||||||||
| if len(matches) != len(assistants): | ||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If one row in a large dataset is malformed (e.g. two assistant messages in a row), this would stop the whole run. Is strict failure intended, or would logging and skipping the example be safer?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strict failure is intended, to match the existing template path. It raises |
||||||||||||||||||||||||||||||||||||||||||
| f"Mismatch between number of assistant messages ({len(assistants)}) " | ||||||||||||||||||||||||||||||||||||||||||
| f"and encoded turns ({len(matches)}). Encoded text: {text!r}" | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| messages = [] | ||||||||||||||||||||||||||||||||||||||||||
| is_prompt = [] | ||||||||||||||||||||||||||||||||||||||||||
| cursor = 0 | ||||||||||||||||||||||||||||||||||||||||||
| for match in matches: | ||||||||||||||||||||||||||||||||||||||||||
| # In thinking mode, encode_messages drops the reasoning of assistant turns before the last user | ||||||||||||||||||||||||||||||||||||||||||
| # message and renders them after `</think>`. Keep those turns in the prompt so that only turns | ||||||||||||||||||||||||||||||||||||||||||
| # with their reasoning are trained. | ||||||||||||||||||||||||||||||||||||||||||
| if thinking_mode == "thinking" and match.group(1) == encoding_dsv4.thinking_end_token: | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
| messages.extend([text[cursor : match.start(2)], match.group(2)]) | ||||||||||||||||||||||||||||||||||||||||||
| is_prompt.extend([True, False]) | ||||||||||||||||||||||||||||||||||||||||||
| cursor = match.end(2) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+363
to
+372
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if cursor != len(text): | ||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||
| f"Encoded text length ({len(text)}) does not match final completion boundary ({cursor}); " | ||||||||||||||||||||||||||||||||||||||||||
| f"the final assistant turn was not matched as a trainable completion: {text[cursor:]!r}" | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| example["is_prompt"] = is_prompt | ||||||||||||||||||||||||||||||||||||||||||
| example[data_column_name] = messages | ||||||||||||||||||||||||||||||||||||||||||
| return example | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def apply_chat_template(example, tokenizer_model, data_column_name, model_name=None): | ||||||||||||||||||||||||||||||||||||||||||
| """Formats conversational data by applying the tokenizer's chat template | ||||||||||||||||||||||||||||||||||||||||||
| and identifying prompt/completion segments for SFT masking. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -309,6 +393,8 @@ def apply_chat_template(example, tokenizer_model, data_column_name): | |||||||||||||||||||||||||||||||||||||||||
| which contains the specific chat template. | ||||||||||||||||||||||||||||||||||||||||||
| data_column_name: The name of the column in the `example` dictionary | ||||||||||||||||||||||||||||||||||||||||||
| that contains the list of messages. | ||||||||||||||||||||||||||||||||||||||||||
| model_name: Optional model name used as a fallback to identify models | ||||||||||||||||||||||||||||||||||||||||||
| that use a custom chat encoder when `tokenizer_model.chat_template` is None. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||
| The modified `example` dictionary. | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -323,6 +409,8 @@ def apply_chat_template(example, tokenizer_model, data_column_name): | |||||||||||||||||||||||||||||||||||||||||
| is_prompt = [] | ||||||||||||||||||||||||||||||||||||||||||
| round_msgs = [] | ||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||
| if getattr(tokenizer_model, "chat_template", None) is None and _is_deepseek_v4(tokenizer_model, model_name): | ||||||||||||||||||||||||||||||||||||||||||
| return _format_conversations_dsv4(example, data_column_name) | ||||||||||||||||||||||||||||||||||||||||||
| for idx, message in enumerate(example[data_column_name]): | ||||||||||||||||||||||||||||||||||||||||||
| if message["role"] == "system": | ||||||||||||||||||||||||||||||||||||||||||
| if idx != 0: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 While
grain_data_processinghastest_format_chat_template_grain_deepseek_v4,hf_data_processinglacks a unit test verifying thatmodel_nameis forwarded toapply_chat_templateor that HuggingFace dataset preprocessing completes without errors for DeepSeek-V4. Add test coverage forhf_data_processing.preprocessing_pipelinewith DeepSeek-V4 and missingchat_template.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
HfDataProcessingTest.test_sft_forwards_model_name_to_apply_chat_templatein2500ee9fc. It runshf_data_processing.preprocessing_pipeline(use_sft=True)and asserts thatapply_chat_templatereceivesmodel_name.