mirror of
https://github.com/hiyouga/LLaMA-Factory.git
synced 2026-10-30 10:50:00 +08:00
[model] add Xing4 model support (#10754)
This commit is contained in:
@@ -338,6 +338,7 @@ Read technical notes:
|
||||
| [Seed (OSS/Coder)](https://huggingface.co/ByteDance-Seed) | 8B/36B | seed_oss/seed_coder |
|
||||
| [StarCoder 2](https://huggingface.co/bigcode) | 3B/7B/15B | - |
|
||||
| [TeleChat 2-2.5](https://huggingface.co/Tele-AI) | 3B/7B/35B/115B | telechat2 |
|
||||
| [Xing4.0-29B-A4B](https://huggingface.co/XingChen-AGI) | 29B-A4B | xing4_0 |
|
||||
| [Yuan 2](https://huggingface.co/IEITYuan) | 2B/51B/102B | yuan |
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -339,6 +339,7 @@ https://github.com/user-attachments/assets/43b700c6-a178-41db-b1f8-8190a5d3fcfc
|
||||
| [Seed (OSS/Coder)](https://huggingface.co/ByteDance-Seed) | 8B/36B | seed_oss/seed_coder |
|
||||
| [StarCoder 2](https://huggingface.co/bigcode) | 3B/7B/15B | - |
|
||||
| [TeleChat 2-2.5](https://huggingface.co/Tele-AI) | 3B/7B/35B/115B | telechat2 |
|
||||
| [Xing4.0-29B-A4B](https://huggingface.co/XingChen-AGI) | 29B-A4B | xing4_0 |
|
||||
| [Yuan 2](https://huggingface.co/IEITYuan) | 2B/51B/102B | yuan |
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -675,6 +675,62 @@ class Glm47ReasoningTemplate(ReasoningTemplate):
|
||||
return self.thought_words[0] + content + self.thought_words[1]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Xing4ReasoningTemplate(ReasoningTemplate):
|
||||
r"""Xing4.0 places the thinking-mode marker in the generation prompt."""
|
||||
|
||||
@override
|
||||
def add_thought(self, content: str = "") -> str:
|
||||
if not self.enable_thinking:
|
||||
return self.thought_words[1] + content
|
||||
|
||||
return self.thought_words[0] + "\n" + self.thought_words[1] + content
|
||||
|
||||
def _move_thought_prefix(
|
||||
self,
|
||||
tokenizer: "PreTrainedTokenizer",
|
||||
prompt_ids: list[int],
|
||||
response_ids: list[int],
|
||||
) -> tuple[list[int], list[int]]:
|
||||
if not self.enable_thinking:
|
||||
return prompt_ids, response_ids
|
||||
|
||||
thought_prefix_ids = tokenizer.encode(self.thought_words[0], add_special_tokens=False)
|
||||
if response_ids[: len(thought_prefix_ids)] == thought_prefix_ids:
|
||||
prompt_ids += thought_prefix_ids
|
||||
response_ids = response_ids[len(thought_prefix_ids) :]
|
||||
|
||||
return prompt_ids, response_ids
|
||||
|
||||
@override
|
||||
def encode_oneturn(
|
||||
self,
|
||||
tokenizer: "PreTrainedTokenizer",
|
||||
messages: list[dict[str, str]],
|
||||
system: Optional[str] = None,
|
||||
tools: Optional[str] = None,
|
||||
) -> tuple[list[int], list[int]]:
|
||||
prompt_ids, response_ids = super().encode_oneturn(tokenizer, messages, system, tools)
|
||||
return self._move_thought_prefix(tokenizer, prompt_ids, response_ids)
|
||||
|
||||
@override
|
||||
def encode_multiturn(
|
||||
self,
|
||||
tokenizer: "PreTrainedTokenizer",
|
||||
messages: list[dict[str, str]],
|
||||
system: Optional[str] = None,
|
||||
tools: Optional[str] = None,
|
||||
discarding_history_cot: bool = False,
|
||||
) -> list[tuple[list[int], list[int]]]:
|
||||
encoded_pairs = super().encode_multiturn(
|
||||
tokenizer, messages, system, tools, discarding_history_cot=discarding_history_cot
|
||||
)
|
||||
return [
|
||||
self._move_thought_prefix(tokenizer, prompt_ids, response_ids)
|
||||
for prompt_ids, response_ids in encoded_pairs
|
||||
]
|
||||
|
||||
|
||||
TEMPLATES: dict[str, "Template"] = {}
|
||||
|
||||
|
||||
@@ -2561,6 +2617,25 @@ register_template(
|
||||
)
|
||||
|
||||
|
||||
register_template(
|
||||
name="xing4_0",
|
||||
format_user=StringFormatter(slots=["<_user>{{content}}<_bot>"]),
|
||||
format_assistant=StringFormatter(slots=["{{content}}", {"eos_token"}, "\n"]),
|
||||
format_function=FunctionFormatter(
|
||||
slots=["{{content}}", {"eos_token"}, "\n"], tool_format="xing4"
|
||||
),
|
||||
format_system=StringFormatter(slots=["{{content}}"]),
|
||||
format_observation=StringFormatter(
|
||||
slots=["<_observation><tool_response>{{content}}</tool_response><_bot>"]
|
||||
),
|
||||
format_prefix=EmptyFormatter(slots=["<_system>"]),
|
||||
format_tools=ToolFormatter(tool_format="xing4"),
|
||||
thought_words=("<think>\n", "</think>"),
|
||||
tool_call_words=("<tool_call>", "</tool_call>"),
|
||||
template_class=Xing4ReasoningTemplate,
|
||||
)
|
||||
|
||||
|
||||
register_template(
|
||||
name="vicuna",
|
||||
format_user=StringFormatter(slots=["USER: {{content}} ASSISTANT:"]),
|
||||
|
||||
@@ -63,6 +63,15 @@ GLM5_NEXT_TOOL_PROMPT = (
|
||||
"<arg_key>{{arg-key-2}}</arg_key><arg_value>{{arg-value-2}}</arg_value>...</tool_call>"
|
||||
)
|
||||
|
||||
XING4_TOOL_PROMPT = (
|
||||
"\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\n"
|
||||
"You are provided with function signatures within <tools></tools> XML tags:\n<tools>{tool_text}"
|
||||
"\n</tools>\n\nFor each function call, output the function name and arguments within the following XML format:\n"
|
||||
"<tool_call>{{function-name}}<param_key>{{param-key-1}}</param_key>"
|
||||
"<param_value>{{param-value-1}}</param_value><param_key>{{param-key-2}}</param_key>"
|
||||
"<param_value>{{param-value-2}}</param_value>...</tool_call>"
|
||||
)
|
||||
|
||||
LLAMA3_TOOL_PROMPT = (
|
||||
"Cutting Knowledge Date: December 2023\nToday Date: {date}\n\n"
|
||||
"You have access to the following functions. To call a function, please respond with JSON for a function call. "
|
||||
@@ -850,6 +859,88 @@ class GLM5NextToolUtils(GLM4MOEToolUtils):
|
||||
return "".join(calls)
|
||||
|
||||
|
||||
class Xing4ToolUtils(ToolUtils):
|
||||
r"""Xing4 tool using template."""
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def tool_formatter(tools: list[dict[str, Any]]) -> str:
|
||||
tool_text = "".join("\n" + json.dumps(tool, ensure_ascii=False) for tool in tools)
|
||||
return XING4_TOOL_PROMPT.format(tool_text=tool_text)
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def function_formatter(functions: list["FunctionCall"]) -> str:
|
||||
calls = []
|
||||
for name, arguments in functions:
|
||||
if not isinstance(name, str) or not re.fullmatch(r"[^\s<>]+", name):
|
||||
raise ValueError("Invalid xing4 function name.")
|
||||
|
||||
arguments = json.loads(arguments)
|
||||
if not isinstance(arguments, dict):
|
||||
raise ValueError("Xing4 function arguments must be a JSON object.")
|
||||
|
||||
call = f"<tool_call>{name}"
|
||||
for key, value in arguments.items():
|
||||
if not re.fullmatch(r"[^<>]+", key):
|
||||
raise ValueError("Invalid xing4 parameter name.")
|
||||
|
||||
value = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
|
||||
call += f"<param_key>{key}</param_key><param_value>{value}</param_value>"
|
||||
|
||||
calls.append(call + "</tool_call>")
|
||||
|
||||
return "".join(calls)
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def tool_extractor(content: str) -> Union[str, list["FunctionCall"]]:
|
||||
call_pattern = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
|
||||
call_matches = list(re.finditer(call_pattern, content))
|
||||
if not call_matches:
|
||||
return content
|
||||
|
||||
if content.count("<tool_call>") != len(call_matches) or content.count("</tool_call>") != len(call_matches):
|
||||
return content
|
||||
|
||||
results = []
|
||||
param_pattern = re.compile(r"<param_key>(.*?)</param_key><param_value>(.*?)</param_value>", re.DOTALL)
|
||||
for call_match in call_matches:
|
||||
call_content = call_match.group(1)
|
||||
name_match = re.match(r"\s*([^\s<>]+)", call_content)
|
||||
if name_match is None:
|
||||
return content
|
||||
|
||||
arguments = {}
|
||||
params_content = call_content[name_match.end() :]
|
||||
position = 0
|
||||
for param_match in re.finditer(param_pattern, params_content):
|
||||
if params_content[position : param_match.start()].strip():
|
||||
return content
|
||||
|
||||
key, raw_value = param_match.groups()
|
||||
key = key.strip()
|
||||
if not key:
|
||||
return content
|
||||
|
||||
try:
|
||||
value = json.loads(raw_value)
|
||||
except json.JSONDecodeError:
|
||||
value = raw_value
|
||||
|
||||
arguments[key] = value
|
||||
position = param_match.end()
|
||||
|
||||
if params_content[position:].strip():
|
||||
return content
|
||||
|
||||
results.append(
|
||||
FunctionCall(name_match.group(1), json.dumps(arguments, ensure_ascii=False))
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
class SeedToolUtils(ToolUtils):
|
||||
r"""Seed tool using template."""
|
||||
|
||||
@@ -1031,6 +1122,7 @@ TOOLS = {
|
||||
"qwen": QwenToolUtils(),
|
||||
"qwen3_5": Qwen35ToolUtils(),
|
||||
"qwen3_8": Qwen38ToolUtils(),
|
||||
"xing4": Xing4ToolUtils(),
|
||||
"glm4_moe": GLM4MOEToolUtils(),
|
||||
"glm5_next": GLM5NextToolUtils(),
|
||||
"seed_oss": SeedToolUtils(),
|
||||
|
||||
@@ -3422,6 +3422,17 @@ register_model_group(
|
||||
)
|
||||
|
||||
|
||||
register_model_group(
|
||||
models={
|
||||
"Xing4.0-29B-A4B": {
|
||||
DownloadSource.DEFAULT: "XingChen-AGI/Xing4.0-29B-A4B",
|
||||
DownloadSource.MODELSCOPE: "XingChen-AGI/Xing4.0-29B-A4B",
|
||||
},
|
||||
},
|
||||
template="xing4_0",
|
||||
)
|
||||
|
||||
|
||||
register_model_group(
|
||||
models={
|
||||
"VibeThinker-1.5B": {
|
||||
|
||||
@@ -60,6 +60,7 @@ def _get_init_kwargs(model_args: "ModelArguments") -> dict[str, Any]:
|
||||
"""
|
||||
skip_check_imports()
|
||||
model_args.model_name_or_path = try_download_model_from_other_hub(model_args)
|
||||
|
||||
return {
|
||||
"trust_remote_code": model_args.trust_remote_code,
|
||||
"cache_dir": model_args.cache_dir,
|
||||
|
||||
@@ -156,6 +156,10 @@ def add_z3_leaf_module(model: "PreTrainedModel") -> None:
|
||||
|
||||
_set_z3_leaf_modules(model, [Qwen3_5MoeSparseMoeBlock])
|
||||
|
||||
if model_type == "xing4_0":
|
||||
# Xing4.0 uses custom code
|
||||
_set_z3_leaf_modules(model, ["Xing4_0MoE"])
|
||||
|
||||
|
||||
def configure_moe(config: "PretrainedConfig", model_args: "ModelArguments", is_trainable: bool) -> None:
|
||||
if not is_trainable or not model_args.moe_aux_loss_coef:
|
||||
|
||||
@@ -369,6 +369,9 @@ def patch_config(
|
||||
init_kwargs: dict[str, Any],
|
||||
is_trainable: bool,
|
||||
) -> None:
|
||||
if getattr(config, "model_type", None) == "xing4_0":
|
||||
check_version("transformers>=5.4.0", mandatory=True)
|
||||
|
||||
if model_args.compute_dtype is None: # priority: bf16 > fp16 > fp32
|
||||
if model_args.infer_dtype != "auto" and not is_trainable:
|
||||
model_args.compute_dtype = getattr(torch, model_args.infer_dtype)
|
||||
|
||||
@@ -31,13 +31,14 @@ Init workflow:
|
||||
|
||||
import torch
|
||||
from accelerate import init_empty_weights
|
||||
from transformers import AutoConfig, AutoProcessor
|
||||
from transformers import AutoConfig, AutoProcessor, AutoTokenizer
|
||||
|
||||
from ..accelerator.helper import DeviceType
|
||||
from ..accelerator.interface import DistributedInterface
|
||||
from ..config.model_args import ModelArguments, ModelClass
|
||||
from ..utils import logging
|
||||
from ..utils.helper import get_tokenizer, is_tokenizer
|
||||
from ..utils.packages import check_version
|
||||
from ..utils.types import HFConfig, HFModel, Processor
|
||||
from .rendering import Renderer
|
||||
|
||||
@@ -97,10 +98,17 @@ class ModelEngine:
|
||||
NOTE: Transformers v5 always use fast tokenizer.
|
||||
https://github.com/huggingface/transformers/blob/v5.0.0rc1/src/transformers/models/auto/tokenization_auto.py#L642
|
||||
"""
|
||||
try:
|
||||
return AutoProcessor.from_pretrained(
|
||||
self.args.model,
|
||||
trust_remote_code=self.args.trust_remote_code,
|
||||
)
|
||||
except Exception:
|
||||
logger.info_rank0("Failed to load processor, falling back to tokenizer.")
|
||||
return AutoTokenizer.from_pretrained(
|
||||
self.args.model,
|
||||
trust_remote_code=self.args.trust_remote_code,
|
||||
)
|
||||
|
||||
def _sync_chat_template(self) -> None:
|
||||
"""Sync chat_template and inject custom_chat_template."""
|
||||
@@ -116,10 +124,14 @@ class ModelEngine:
|
||||
|
||||
def _init_model_config(self) -> HFConfig:
|
||||
"""Init model config."""
|
||||
return AutoConfig.from_pretrained(
|
||||
config = AutoConfig.from_pretrained(
|
||||
self.args.model,
|
||||
trust_remote_code=self.args.trust_remote_code,
|
||||
)
|
||||
if getattr(config, "model_type", None) == "xing4_0":
|
||||
check_version("transformers>=5.4.0", mandatory=True)
|
||||
|
||||
return config
|
||||
|
||||
def _init_model(self) -> HFModel:
|
||||
"""Init model.
|
||||
|
||||
@@ -302,6 +302,65 @@ def test_qwen_multi_tool_extractor():
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
def test_xing4_function_formatter():
|
||||
formatter = FunctionFormatter(slots=["{{content}}", "<_end>\n"], tool_format="xing4")
|
||||
tool_calls = json.dumps(
|
||||
{
|
||||
"name": "tool_name",
|
||||
"arguments": {"foo": "bar", "size": 10, "enabled": True, "items": [1, 2]},
|
||||
}
|
||||
)
|
||||
assert formatter.apply(content=tool_calls) == [
|
||||
"<tool_call>tool_name<param_key>foo</param_key><param_value>bar</param_value>"
|
||||
"<param_key>size</param_key><param_value>10</param_value>"
|
||||
"<param_key>enabled</param_key><param_value>true</param_value>"
|
||||
"<param_key>items</param_key><param_value>[1, 2]</param_value></tool_call>",
|
||||
"<_end>\n",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
def test_xing4_multi_function_formatter():
|
||||
formatter = FunctionFormatter(slots=["{{content}}"], tool_format="xing4")
|
||||
tool_calls = json.dumps([FUNCTION, {"name": "another_tool", "arguments": {"city": "北京"}}])
|
||||
assert formatter.apply(content=tool_calls) == [
|
||||
"<tool_call>tool_name<param_key>foo</param_key><param_value>bar</param_value>"
|
||||
"<param_key>size</param_key><param_value>10</param_value></tool_call>"
|
||||
"<tool_call>another_tool<param_key>city</param_key><param_value>北京</param_value></tool_call>"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
def test_xing4_tool_formatter():
|
||||
formatter = ToolFormatter(tool_format="xing4")
|
||||
assert formatter.apply(content=json.dumps(TOOLS)) == [
|
||||
"\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\n"
|
||||
"You are provided with function signatures within <tools></tools> XML tags:\n<tools>"
|
||||
f"\n{json.dumps(TOOLS[0], ensure_ascii=False)}"
|
||||
"\n</tools>\n\nFor each function call, output the function name and arguments within the following "
|
||||
"XML format:\n<tool_call>{function-name}<param_key>{param-key-1}</param_key>"
|
||||
"<param_value>{param-value-1}</param_value><param_key>{param-key-2}</param_key>"
|
||||
"<param_value>{param-value-2}</param_value>...</tool_call>"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
def test_xing4_multi_tool_extractor():
|
||||
formatter = ToolFormatter(tool_format="xing4")
|
||||
result = (
|
||||
"<think>check tools</think>"
|
||||
"<tool_call>test_tool<param_key>foo</param_key><param_value>bar</param_value>"
|
||||
"<param_key>size</param_key><param_value>10</param_value></tool_call>"
|
||||
"<tool_call>another_tool<param_key>enabled</param_key><param_value>true</param_value>"
|
||||
"<param_key>items</param_key><param_value>[1, 2]</param_value></tool_call>"
|
||||
)
|
||||
assert formatter.extract(result) == [
|
||||
("test_tool", """{"foo": "bar", "size": 10}"""),
|
||||
("another_tool", """{"enabled": true, "items": [1, 2]}"""),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
def test_lfm2_function_formatter():
|
||||
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="lfm2")
|
||||
|
||||
Reference in New Issue
Block a user