diff --git a/README.md b/README.md index ec7ae7f81..3d2b40eca 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ sh INSTALL_MEGATRON.sh | Server startup scripts | transformers/megatron | [Script](cookbook/client/server) | ## Changelog +- 🎉2026-08-12 The ModelScope training service has been deployed to [Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B). - 🎉2026-08-04 Sandboxed multi-turn RL is now supported: run model-generated code in isolated [AgentENV](https://github.com/kvcache-ai/AgentENV) Firecracker microVMs, or in an OpenEnv server, with the same `train.py`. See the [cookbook](cookbook/rl/envs) and the [deployment guide](docs/source_en/Usage%20Guide/Agentic-RL-Deployment-and-Training.md). - 🎉2026-05-20 Support DeepSeek-V4-Flash and DeepSeek-V4-Pro models. - 🎉2026-05-20 Multi-turn rollout and tool calling in RL are now supported. The Cookbook is currently being written. You can use `from twinkle_agentic.rollout import MultiTurnRollout/APIMultiTurnRollout` directly for multi-turn rollout. @@ -142,13 +143,16 @@ supported on Twinkle✨ framework. > For serverless training service accessed via `base_url=https://www.modelscope.cn/twinkle`, it > is currently provided via the Tinker-compatible APIs. We will be rolling out services that support > both Tinker APIs, as well as the full-fledged Twinkle✨ native APIs. The serverless endpoint is backed -> by one training base at a time, and currently it is [Qwen3.6-27B](https://modelscope.cn/models/Qwen/Qwen3.6-27B). +> by one training base at a time, and currently it is [Qwen3.8-27B](https://modelscope.cn/models/Qwen/Qwen3.8-27B). | Model Type | Model ID on [ModelScope](https://modelscope.cn) | Model Size | Requires | Support Megatron | HF Model ID | |---------------------|-----------------------------------------------------------------------------------------------------------------|:---------------------------------------:|----------------------|:----------------:|:---------------------------------------------------------------------------------------------------------:| | qwen3 series | [Qwen/Qwen3-14B-Base](https://modelscope.cn/models/Qwen/Qwen3-14B-Base) | 0.6B/1.7B/4B/8B/14B | transformers>=4.51 | ✔ | [Qwen/Qwen3-14B-Base](https://huggingface.co/Qwen/Qwen3-14B-Base) | | | [Qwen/Qwen3-32B](https://modelscope.cn/models/Qwen/Qwen3-32B) | 0.6B/1.7B/4B/8B/14B/32B | transformers>=4.51 | ✔ | [Qwen/Qwen3-32B](https://huggingface.co/Qwen/Qwen3-32B) | | qwen3_moe series | [Qwen/Qwen3-30B-A3B-Base](https://modelscope.cn/models/Qwen/Qwen3-30B-A3B-Base) | 30B-A3B/A3B-Base,235B-A22B | transformers>=4.51 | ✔ | [Qwen/Qwen3-30B-A3B-Base](https://huggingface.co/Qwen/Qwen3-30B-A3B-Base) | +| qwen3.8 series | [Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B) | 27B | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) | +| qwen3.6 moe series | [Qwen/Qwen3.6-35B-A3B](https://www.modelscope.cn/models/Qwen/Qwen3.6-35B-A3B) | 35B-A3B, etc. | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.6-35B-A3B](https://huggingface.co/Qwen/Qwen3.6-35B-A3B) | +| qwen3.6 series | [Qwen/Qwen3.6-27B](https://www.modelscope.cn/models/Qwen/Qwen3.6-27B) | 4B ~ 27B | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B) | | qwen3.5 moe series | [Qwen/Qwen3.5-35B-A3B](https://www.modelscope.cn/models/Qwen/Qwen3.5-35B-A3B) | 35B-A3B,122B-A10B, etc. | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.5-35B-A3B](https://huggingface.co/Qwen/Qwen3.5-35B-A3B) | | qwen3.5 series | [Qwen/Qwen3.5-9B](https://www.modelscope.cn/models/Qwen/Qwen3.5-9B) | 2B ~ 27B | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.5-9B](https://huggingface.co/Qwen/Qwen3.5-9B) | | qwen2 series | [Qwen/Qwen2-0.5B-Instruct](https://modelscope.cn/models/Qwen/Qwen2-0.5B-Instruct) | 0.5B/1.5B/7B/72B | transformers>=4.37 | ✔ | [Qwen/Qwen2-0.5B-Instruct](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) | @@ -254,7 +258,7 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -base_model = 'ms://Qwen/Qwen3.6-27B' +base_model = 'ms://Qwen/Qwen3.8-27B' base_url='your-base-url' api_key='your-api-key' diff --git a/README_ZH.md b/README_ZH.md index 51de62dd1..f2f214f48 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -102,6 +102,7 @@ sh INSTALL_MEGATRON.sh Twinkle✨支持相同的算法接口运行在单GPU、torchrun多机、Ray、Client等各场景下。其算法过程是外露的,非常便于修改和调试。完整的框架介绍请查看[快速开始](https://modelscope.github.io/twinkle-web/zh/docs/usage-guide/quick-start/) ## 更新日志 +- 🎉2026-08-12 ModelScope的训练服务部署为[Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B)。 - 🎉2026-08-04 支持沙箱环境下的多轮RL训练:模型生成的代码可在隔离的 [AgentENV](https://github.com/kvcache-ai/AgentENV) Firecracker microVM 或 OpenEnv 服务中执行,两个后端共用同一份 `train.py`。参考 [cookbook](cookbook/rl/envs) 和[部署文档](docs/source_zh/使用指引/Agentic%20RL部署与训练.md)。 - 🎉2026-05-20 支持DeepSeek-V4-Flash and DeepSeek-V4-Pro系列模型。 - 🎉2026-05-20 支持多轮rollout和RL中的工具调用,Cookbook正在编写中,可以直接使用`from twinkle_agentic.rollout import MultiTurnRollout/APIMultiTurnRollout`进行多轮rollout。 @@ -132,13 +133,16 @@ Twinkle✨支持相同的算法接口运行在单GPU、torchrun多机、Ray、Cl 随着新模型的发布,我们将添加对更多模型的支持。下表列出了 Twinkle✨ 框架当前支持的模型。 >[!Note] -> 通过 `base_url=https://www.modelscope.cn/twinkle` 访问的无服务器训练服务,目前是通过兼容Tinker的API提供的。我们将陆续推出同时支持Tinker API和完整Twinkle✨原生 API的服务。无服务器端点每次由一个训练基座支持,目前使用的是[Qwen3.6-27B](https://modelscope.cn/models/Qwen/Qwen3.6-27B)。 +> 通过 `base_url=https://www.modelscope.cn/twinkle` 访问的无服务器训练服务,目前是通过兼容Tinker的API提供的。我们将陆续推出同时支持Tinker API和完整Twinkle✨原生 API的服务。无服务器端点每次由一个训练基座支持,目前使用的是[Qwen3.8-27B](https://modelscope.cn/models/Qwen/Qwen3.8-27B)。 | Model Type | Model ID 举例 | Model Size | Requires | Support Megatron | HF Model ID | |---------------------|-----------------------------------------------------------------------------------------------------------------|:---------------------------------------:|----------------------|:----------------:|:---------------------------------------------------------------------------------------------------------:| | qwen3 全系列 | [Qwen/Qwen3-14B-Base](https://modelscope.cn/models/Qwen/Qwen3-14B-Base) | 0.6B/1.7B/4B/8B/14B | transformers>=4.51 | ✔ | [Qwen/Qwen3-14B-Base](https://huggingface.co/Qwen/Qwen3-14B-Base) | | | [Qwen/Qwen3-32B](https://modelscope.cn/models/Qwen/Qwen3-32B) | 0.6B/1.7B/4B/8B/14B/32B | transformers>=4.51 | ✔ | [Qwen/Qwen3-32B](https://huggingface.co/Qwen/Qwen3-32B) | | qwen3_moe 全系列 | [Qwen/Qwen3-30B-A3B-Base](https://modelscope.cn/models/Qwen/Qwen3-30B-A3B-Base) | 30B-A3B/A3B-Base,235B-A22B | transformers>=4.51 | ✔ | [Qwen/Qwen3-30B-A3B-Base](https://huggingface.co/Qwen/Qwen3-30B-A3B-Base) | +| qwen3.8 全系列 | [Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B) | 27B | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) | +| qwen3.6 moe 全系列 | [Qwen/Qwen3.6-35B-A3B](https://www.modelscope.cn/models/Qwen/Qwen3.6-35B-A3B) | 35B-A3B, etc. | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.6-35B-A3B](https://huggingface.co/Qwen/Qwen3.6-35B-A3B) | +| qwen3.6 全系列 | [Qwen/Qwen3.6-27B](https://www.modelscope.cn/models/Qwen/Qwen3.6-27B) | 4B ~ 27B | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B) | | qwen3.5 moe 全系列 | [Qwen/Qwen3.5-35B-A3B](https://www.modelscope.cn/models/Qwen/Qwen3.5-35B-A3B) | 35B-A3B,122B-A10B, etc. | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.5-35B-A3B](https://huggingface.co/Qwen/Qwen3.5-35B-A3B) | | qwen3.5 全系列 | [Qwen/Qwen3.5-9B](https://www.modelscope.cn/models/Qwen/Qwen3.5-9B) | 2B ~ 27B | transformers>=5.2.0 | ✔ | [Qwen/Qwen3.5-9B](https://huggingface.co/Qwen/Qwen3.5-9B) | | qwen2 全系列 | [Qwen/Qwen2-0.5B-Instruct](https://modelscope.cn/models/Qwen/Qwen2-0.5B-Instruct) | 0.5B/1.5B/7B/72B | transformers>=4.37 | ✔ | [Qwen/Qwen2-0.5B-Instruct](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) | @@ -243,7 +247,7 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -base_model = 'ms://Qwen/Qwen3.6-27B' +base_model = 'ms://Qwen/Qwen3.8-27B' base_url='your-base-url' api_key='your-api-key' diff --git a/cookbook/client/server/megatron/server_config.yaml b/cookbook/client/server/megatron/server_config.yaml index 67ef5fdaa..759601985 100644 --- a/cookbook/client/server/megatron/server_config.yaml +++ b/cookbook/client/server/megatron/server_config.yaml @@ -39,7 +39,7 @@ applications: server_config: per_token_model_limit: 3 # Maximum number of models (adapters) per token (server-globally enforced) supported_models: - - Qwen/Qwen3.6-27B + - Qwen/Qwen3.8-27B deployments: - name: TinkerCompatServer @@ -59,11 +59,11 @@ applications: # 3. Sampler Service - Runs inference / sampling using vLLM engine # Used for generating text from the model (e.g., evaluating LoRA results). # Config: TP=2 x DP=2 on 4 GPUs, ~27GB weights/GPU, ~37GB for KV cache + LoRA - - name: sampler-Qwen3.6-27B - route_prefix: /api/v1/sampler/Qwen/Qwen3.6-27B + - name: sampler-Qwen3.8-27B + route_prefix: /api/v1/sampler/Qwen/Qwen3.8-27B import_path: sampler args: - model_id: "ms://Qwen/Qwen3.6-27B" # ModelScope model identifier + model_id: "ms://Qwen/Qwen3.8-27B" # ModelScope model identifier nproc_per_node: 4 # Number of GPU processes per node sampler_type: vllm # Inference engine: 'vllm' (fast) or 'torch' (TorchSampler) engine_args: # vLLM engine-specific settings @@ -102,12 +102,12 @@ applications: # 2. Model Service - Hosts the base model for training. # Config: PP=2 x DP=2 on 4 GPUs, ~27GB weights/GPU, comfortable for LoRA training - - name: models-Qwen3.6-27B - route_prefix: /api/v1/model/Qwen/Qwen3.6-27B + - name: models-Qwen3.8-27B + route_prefix: /api/v1/model/Qwen/Qwen3.8-27B import_path: model args: backend: megatron # Use Megatron-LM backend - model_id: "ms://Qwen/Qwen3.6-27B" # ModelScope model identifier + model_id: "ms://Qwen/Qwen3.8-27B" # ModelScope model identifier max_length: 32768 # model max length max_loras: 3 # model max loras nproc_per_node: 4 # Number of GPU processes per node diff --git a/docs/source_en/Components/Model/SupportedModels.md b/docs/source_en/Components/Model/SupportedModels.md index 7cd9e8b4d..3e868816a 100644 --- a/docs/source_en/Components/Model/SupportedModels.md +++ b/docs/source_en/Components/Model/SupportedModels.md @@ -6,6 +6,8 @@ Twinkle supports any model compatible with HuggingFace Transformers or Megatron- | Model Family | Model IDs | Parameters | Features | |:-------------|:----------|:-----------|:---------| +| Qwen 3.8 | `Qwen/Qwen3.8-27B` | 27B | Thinking mode | +| Qwen 3.6 | `Qwen/Qwen3.6-27B`, `Qwen/Qwen3.6-35B-A3B` | 27B, 35B-A3B | MoE, Thinking mode | | Qwen 3.5 | `Qwen/Qwen3.5-0.6B` ~ `Qwen/Qwen3.5-235B-A22B` | 0.6B–235B | MoE, Thinking mode | | Qwen 2.5 | `Qwen/Qwen2.5-0.5B` ~ `Qwen/Qwen2.5-72B` | 0.5B–72B | Dense | | DeepSeek V4 | `deepseek-ai/DeepSeek-V4` | 685B MoE | Custom DSML encoding | diff --git a/docs/source_en/Usage Guide/Introduction-with-Qwen3.5.md b/docs/source_en/Usage Guide/Introduction-with-Qwen3.5.md index 6f402823d..22dc07dfc 100644 --- a/docs/source_en/Usage Guide/Introduction-with-Qwen3.5.md +++ b/docs/source_en/Usage Guide/Introduction-with-Qwen3.5.md @@ -465,9 +465,9 @@ init_tinker_client() from tinker import ServiceClient -# Base model -base_model = 'Qwen/Qwen3.5-4B' -base_url = 'http://www.modelscope.cn/twinkle' +# Base model (currently deployed in the ModelScope official environment) +base_model = 'Qwen/Qwen3.8-27B' +base_url = 'https://www.modelscope.cn/twinkle' def train(): @@ -530,7 +530,7 @@ Alongside the open-source release of Twinkle, ModelScope provides a hosted model ```python base_url = 'https://www.modelscope.cn/twinkle' -base_model = 'Qwen/Qwen3.5-4B' # Model currently deployed in the official environment +base_model = 'Qwen/Qwen3.8-27B' # Model currently deployed in the official environment ``` --- diff --git a/docs/source_en/Usage Guide/Server and Client/Overview.md b/docs/source_en/Usage Guide/Server and Client/Overview.md index 3c45673f0..b09d86145 100644 --- a/docs/source_en/Usage Guide/Server and Client/Overview.md +++ b/docs/source_en/Usage Guide/Server and Client/Overview.md @@ -106,5 +106,5 @@ The same examples work with the ModelScope managed service by changing only the ```bash export TWINKLE_SERVER_URL=https://www.modelscope.cn/twinkle export TWINKLE_SERVER_TOKEN="$MODELSCOPE_TOKEN" -export TWINKLE_MODEL_ID=Qwen/Qwen3.6-27B +export TWINKLE_MODEL_ID=Qwen/Qwen3.8-27B ``` diff --git a/docs/source_en/Usage Guide/Train-as-a-Service.md b/docs/source_en/Usage Guide/Train-as-a-Service.md index 57ba26fbc..286b52001 100644 --- a/docs/source_en/Usage Guide/Train-as-a-Service.md +++ b/docs/source_en/Usage Guide/Train-as-a-Service.md @@ -2,7 +2,7 @@ Alongside the open-source release of the Twinkle framework, we also provide a hosted model training service (Training as a Service) powered by ModelScope's backend infrastructure. Developers can use this service to experience Twinkle's training API for free. -The model currently running on the cluster is [Qwen/Qwen3.6-27B](https://www.modelscope.cn/models/Qwen/Qwen3.6-27B). Below are the detailed usage instructions: +The model currently running on the cluster is [Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B). Below are the detailed usage instructions: ## Step 1. Register a ModelScope Account and Obtain Your API Key @@ -30,7 +30,7 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -base_model = 'ms://Qwen/Qwen3.6-27B' +base_model = 'ms://Qwen/Qwen3.8-27B' base_url='https://www.modelscope.cn/twinkle' api_key=os.environ.get('MODELSCOPE_TOKEN') @@ -64,7 +64,7 @@ for epoch in range(2): print(f'Saved checkpoint for epoch {epoch} to {result.path}') ``` -With the code above, you can train a self-cognition LoRA based on `Qwen/Qwen3.6-27B`. This LoRA will change the model's name and creator to the names specified during training. To perform inference using this LoRA: +With the code above, you can train a self-cognition LoRA based on `Qwen/Qwen3.8-27B`. This LoRA will change the model's name and creator to the names specified during training. To perform inference using this LoRA: ```python import os @@ -79,7 +79,7 @@ init_tinker_client() from tinker import ServiceClient -base_model = 'Qwen/Qwen3.6-27B' +base_model = 'Qwen/Qwen3.8-27B' base_url = 'https://www.modelscope.cn/twinkle' # Step 2: Define the base model and connect to the server @@ -92,7 +92,7 @@ service_client = ServiceClient( # The model_path is a twinkle:// URI pointing to a previously saved LoRA checkpoint. # The server will load the base model and apply the LoRA adapter weights. sampling_client = service_client.create_sampling_client( - model_path='twinkle://xxx-Qwen_Qwen3.6-35B-A3B-xxx/weights/twinkle-lora-1', + model_path='twinkle://xxx-Qwen_Qwen3.8-27B-xxx/weights/twinkle-lora-1', base_model=base_model ) diff --git "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/Qwen3.5\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/Qwen3.5\346\234\200\344\275\263\345\256\236\350\267\265.md" index fd5561085..c80b1e15d 100644 --- "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/Qwen3.5\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/Qwen3.5\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -465,9 +465,9 @@ init_tinker_client() from tinker import ServiceClient -# 基座模型 -base_model = 'Qwen/Qwen3.5-4B' -base_url = 'http://www.modelscope.cn/twinkle' +# 基座模型(魔搭官方环境当前部署的模型) +base_model = 'Qwen/Qwen3.8-27B' +base_url = 'https://www.modelscope.cn/twinkle' def train(): @@ -530,7 +530,7 @@ Twinkle 框架开源的同时,魔搭社区依托自身算力基础设施,提 ```python base_url = 'https://www.modelscope.cn/twinkle' -base_model = 'Qwen/Qwen3.5-4B' # 官方环境当前部署的模型 +base_model = 'Qwen/Qwen3.8-27B' # 官方环境当前部署的模型 ``` --- diff --git "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\346\234\215\345\212\241\347\253\257\345\222\214\345\256\242\346\210\267\347\253\257/\346\246\202\350\277\260.md" "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\346\234\215\345\212\241\347\253\257\345\222\214\345\256\242\346\210\267\347\253\257/\346\246\202\350\277\260.md" index 5d3044e0c..eace748c1 100644 --- "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\346\234\215\345\212\241\347\253\257\345\222\214\345\256\242\346\210\267\347\253\257/\346\246\202\350\277\260.md" +++ "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\346\234\215\345\212\241\347\253\257\345\222\214\345\256\242\346\210\267\347\253\257/\346\246\202\350\277\260.md" @@ -106,5 +106,5 @@ python cookbook/client/twinkle/self_cognition.py ```bash export TWINKLE_SERVER_URL=https://www.modelscope.cn/twinkle export TWINKLE_SERVER_TOKEN="$MODELSCOPE_TOKEN" -export TWINKLE_MODEL_ID=Qwen/Qwen3.6-27B +export TWINKLE_MODEL_ID=Qwen/Qwen3.8-27B ``` diff --git "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\350\256\255\347\273\203\346\234\215\345\212\241.md" "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\350\256\255\347\273\203\346\234\215\345\212\241.md" index c5db28d3f..c57cf066b 100644 --- "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\350\256\255\347\273\203\346\234\215\345\212\241.md" +++ "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\350\256\255\347\273\203\346\234\215\345\212\241.md" @@ -3,7 +3,7 @@ 在 Twinkle 框架开源的同时,我们依托ModelScope的后台服务,也提供了托管的模型训练服务(Training as a Service),开发者可以通过这一服务, 免费体验Twinkle的训练API。 -目前在集群中运行的模型是[Qwen/Qwen3.6-27B](https://www.modelscope.cn/models/Qwen/Qwen3.6-27B)。下面介绍具体的使用方法: +目前在集群中运行的模型是[Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B)。下面介绍具体的使用方法: ## Step 1. 注册ModelScope用户并获取 API Key @@ -31,7 +31,7 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -base_model = 'ms://Qwen/Qwen3.6-27B' +base_model = 'ms://Qwen/Qwen3.8-27B' base_url='https://www.modelscope.cn/twinkle' api_key=os.environ.get('MODELSCOPE_TOKEN') @@ -65,7 +65,7 @@ for epoch in range(2): print(f'Saved checkpoint for epoch {epoch} to {result.path}') ``` -通过上述代码,你可以训练一个原模型为`Qwen/Qwen3.6-27B`的自我认知lora。这个lora会改变模型的名称和制造者为训练时指定的名称。使用这个lora进行推理: +通过上述代码,你可以训练一个原模型为`Qwen/Qwen3.8-27B`的自我认知lora。这个lora会改变模型的名称和制造者为训练时指定的名称。使用这个lora进行推理: ```python import os @@ -80,7 +80,7 @@ init_tinker_client() from tinker import ServiceClient -base_model = 'Qwen/Qwen3.6-27B' +base_model = 'Qwen/Qwen3.8-27B' base_url = 'https://www.modelscope.cn/twinkle' # Step 2: Define the base model and connect to the server @@ -93,7 +93,7 @@ service_client = ServiceClient( # The model_path is a twinkle:// URI pointing to a previously saved LoRA checkpoint. # The server will load the base model and apply the LoRA adapter weights. sampling_client = service_client.create_sampling_client( - model_path='twinkle://xxx-Qwen_Qwen3.6-35B-A3B-xxx/weights/twinkle-lora-1', + model_path='twinkle://xxx-Qwen_Qwen3.8-27B-xxx/weights/twinkle-lora-1', base_model=base_model ) diff --git "a/docs/source_zh/\347\273\204\344\273\266/\346\250\241\345\236\213/SupportedModels.md" "b/docs/source_zh/\347\273\204\344\273\266/\346\250\241\345\236\213/SupportedModels.md" index bfbb03ea0..f932ff8a1 100644 --- "a/docs/source_zh/\347\273\204\344\273\266/\346\250\241\345\236\213/SupportedModels.md" +++ "b/docs/source_zh/\347\273\204\344\273\266/\346\250\241\345\236\213/SupportedModels.md" @@ -6,6 +6,8 @@ Twinkle 支持任何兼容 HuggingFace Transformers 或 Megatron-LM 的模型。 | 模型系列 | 模型 ID | 参数量 | 特性 | |:---------|:--------|:-------|:-----| +| Qwen 3.8 | `Qwen/Qwen3.8-27B` | 27B | 思考模式 | +| Qwen 3.6 | `Qwen/Qwen3.6-27B`, `Qwen/Qwen3.6-35B-A3B` | 27B、35B-A3B | MoE、思考模式 | | Qwen 3.5 | `Qwen/Qwen3.5-0.6B` ~ `Qwen/Qwen3.5-235B-A22B` | 0.6B–235B | MoE、思考模式 | | Qwen 2.5 | `Qwen/Qwen2.5-0.5B` ~ `Qwen/Qwen2.5-72B` | 0.5B–72B | Dense | | DeepSeek V4 | `deepseek-ai/DeepSeek-V4` | 685B MoE | 自定义 DSML 编码 | diff --git a/notebook/dpo.ipynb b/notebook/dpo.ipynb index caeb65824..d2a13cfa5 100644 --- a/notebook/dpo.ipynb +++ b/notebook/dpo.ipynb @@ -95,7 +95,7 @@ "\n", "| 配置项 | 默认值 | 说明 |\n", "|--------|--------|------|\n", - "| `BASE_MODEL` | Qwen/Qwen3.6-27B | 基座模型 |\n", + "| `BASE_MODEL` | Qwen/Qwen3.8-27B | 基座模型 |\n", "| `BATCH_SIZE` | 4 | 每步处理的 DPO 样本对数 |\n", "| `LEARNING_RATE` | 1e-4 | 学习率 |\n", "| `DPO_BETA` | 0.1 | DPO 温度系数,控制偏好强度 |\n", @@ -149,8 +149,8 @@ "logger = get_logger()\n", "\n", "# ========== 全局配置 ==========\n", - "BASE_MODEL = 'Qwen/Qwen3.6-27B'\n", - "BASE_URL = 'http://www.modelscope.cn/twinkle'\n", + "BASE_MODEL = 'Qwen/Qwen3.8-27B'\n", + "BASE_URL = 'https://www.modelscope.cn/twinkle'\n", "API_KEY = getpass(\"ModelScope Token: \")\n", "DATASET_ID = 'ms://hjh0119/shareAI-Llama3-DPO-zh-en-emoji'\n", "\n", @@ -484,7 +484,7 @@ "\n", "logger = get_logger()\n", "\n", - "BASE_MODEL = 'Qwen/Qwen3.6-27B'\n", + "BASE_MODEL = 'Qwen/Qwen3.8-27B'\n", "\n", "# TODO: 替换为训练输出的检查点路径\n", "weight_path = '<替换为你的 twinkle:// 检查点路径>' # 例如: save_result.path\n", @@ -493,7 +493,7 @@ "from tinker import ServiceClient\n", "\n", "service_client = ServiceClient(\n", - " base_url='http://www.modelscope.cn/twinkle',\n", + " base_url='https://www.modelscope.cn/twinkle',\n", " api_key=API_KEY,\n", ")\n", "\n", @@ -548,7 +548,7 @@ "from peft import PeftModel\n", "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "\n", - "base_model_id = 'Qwen/Qwen3.6-27B'\n", + "base_model_id = 'Qwen/Qwen3.8-27B'\n", "lora_path = '<替换为你的 LoRA 检查点路径>'\n", "output_dir = '<替换为输出目录>'\n", "\n", @@ -582,7 +582,7 @@ "vllm serve <输出目录> --tensor-parallel-size 2\n", "```\n", "\n", - "> **提示**:对于 Dense 模型(如 Qwen3.6-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" + "> **提示**:对于 Dense 模型(如 Qwen3.8-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" ], "id": "peft_merge_2" }, diff --git a/notebook/multi_modal.ipynb b/notebook/multi_modal.ipynb index 4c88d5855..9bb52467f 100644 --- a/notebook/multi_modal.ipynb +++ b/notebook/multi_modal.ipynb @@ -31,7 +31,7 @@ "\n", "| 条件 | 说明 |\n", "|------|------|\n", - "| 服务端已启动 | 需支持多模态模型(如 Qwen3.6 系列) |\n", + "| 服务端已启动 | 需支持多模态模型(如 Qwen3.8 系列) |\n", "| 环境变量 | `.env` 文件或环境中设置 `MODELSCOPE_TOKEN` |\n", "| 依赖安装 | `pip install twinkle-kit[tinker]` |\n", "\n", @@ -149,8 +149,8 @@ "metadata": {}, "outputs": [], "source": [ - "base_model = 'Qwen/Qwen3.6-27B'\n", - "base_url = 'http://www.modelscope.cn/twinkle'\n", + "base_model = 'Qwen/Qwen3.8-27B'\n", + "base_url = 'https://www.modelscope.cn/twinkle'\n", "\n", "client = init_twinkle_client(base_url=base_url, api_key=api_key)\n", "\n", @@ -413,7 +413,7 @@ "\n", "logger = get_logger()\n", "\n", - "BASE_MODEL = 'Qwen/Qwen3.6-27B'\n", + "BASE_MODEL = 'Qwen/Qwen3.8-27B'\n", "\n", "# TODO: 替换为训练输出的检查点路径\n", "weight_path = '<替换为你的 twinkle:// 检查点路径>'\n", @@ -422,7 +422,7 @@ "from tinker import ServiceClient\n", "\n", "service_client = ServiceClient(\n", - " base_url='http://www.modelscope.cn/twinkle',\n", + " base_url='https://www.modelscope.cn/twinkle',\n", " api_key=api_key,\n", ")\n", "\n", @@ -476,7 +476,7 @@ "from peft import PeftModel\n", "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "\n", - "base_model_id = 'Qwen/Qwen3.6-27B'\n", + "base_model_id = 'Qwen/Qwen3.8-27B'\n", "lora_path = '<替换为你的 LoRA 检查点路径>'\n", "output_dir = '<替换为输出目录>'\n", "\n", @@ -510,7 +510,7 @@ "vllm serve <输出目录> --tensor-parallel-size 2\n", "```\n", "\n", - "> **提示**:对于 Dense 模型(如 Qwen3.6-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" + "> **提示**:对于 Dense 模型(如 Qwen3.8-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" ], "id": "peft_merge_2" }, diff --git a/notebook/sample.ipynb b/notebook/sample.ipynb index 34bd2d974..5c3c18b1b 100644 --- a/notebook/sample.ipynb +++ b/notebook/sample.ipynb @@ -108,7 +108,7 @@ "\n", "logger = get_logger()\n", "\n", - "BASE_MODEL = 'Qwen/Qwen3.6-27B'\n", + "BASE_MODEL = 'Qwen/Qwen3.8-27B'\n", "\n", "# TODO: 替换为你的训练检查点路径\n", "weight_path = '<替换为你的 twinkle:// 检查点路径>' # 例如: 'twinkle://xxx/weights/twinkle-lora-2'\n", @@ -139,7 +139,7 @@ "from tinker import ServiceClient\n", "\n", "service_client = ServiceClient(\n", - " base_url='http://www.modelscope.cn/twinkle',\n", + " base_url='https://www.modelscope.cn/twinkle',\n", " api_key=api_key,\n", ")\n", "\n", @@ -261,7 +261,7 @@ "from peft import PeftModel\n", "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "\n", - "base_model_id = 'Qwen/Qwen3.6-27B'\n", + "base_model_id = 'Qwen/Qwen3.8-27B'\n", "lora_path = '<替换为你的 LoRA 检查点路径>'\n", "output_dir = '<替换为输出目录>'\n", "\n", @@ -295,7 +295,7 @@ "vllm serve <输出目录> --tensor-parallel-size 2\n", "```\n", "\n", - "> **提示**:对于 Dense 模型(如 Qwen3.6-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" + "> **提示**:对于 Dense 模型(如 Qwen3.8-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" ], "id": "peft_merge_2" }, diff --git a/notebook/self_cognition.ipynb b/notebook/self_cognition.ipynb index 5b1d91a75..5e3383f13 100644 --- a/notebook/self_cognition.ipynb +++ b/notebook/self_cognition.ipynb @@ -153,8 +153,8 @@ "\n", "from tinker import ServiceClient\n", "\n", - "base_model = 'Qwen/Qwen3.6-27B'\n", - "base_url = 'http://www.modelscope.cn/twinkle'\n", + "base_model = 'Qwen/Qwen3.8-27B'\n", + "base_url = 'https://www.modelscope.cn/twinkle'\n", "api_key = getpass(\"ModelScope Token: \")" ] }, @@ -427,7 +427,7 @@ "from peft import PeftModel\n", "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "\n", - "base_model_id = 'Qwen/Qwen3.6-27B'\n", + "base_model_id = 'Qwen/Qwen3.8-27B'\n", "lora_path = '<替换为你的 LoRA 检查点路径>'\n", "output_dir = '<替换为输出目录>'\n", "\n", @@ -461,7 +461,7 @@ "vllm serve <输出目录> --tensor-parallel-size 2\n", "```\n", "\n", - "> **提示**:对于 Dense 模型(如 Qwen3.6-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" + "> **提示**:对于 Dense 模型(如 Qwen3.8-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" ] }, { diff --git a/notebook/short_math_grpo.ipynb b/notebook/short_math_grpo.ipynb index f7344bcfc..a05766789 100644 --- a/notebook/short_math_grpo.ipynb +++ b/notebook/short_math_grpo.ipynb @@ -90,7 +90,7 @@ "\n", "| 配置项 | 默认值 | 说明 |\n", "|--------|--------|------|\n", - "| `MODEL_ID` | ms://Qwen/Qwen3.6-27B | 基座模型(需加 `ms://` 前缀) |\n", + "| `MODEL_ID` | ms://Qwen/Qwen3.8-27B | 基座模型(需加 `ms://` 前缀) |\n", "| `NUM_GENERATIONS` | 4 | 每个 prompt 生成几条回答 |\n", "| `MAX_NEW_TOKENS` | 1024 | 单条回答最大 token 数 |\n", "| `LEARNING_RATE` | 2e-5 | 学习率 |\n", @@ -149,7 +149,7 @@ "logger = get_logger()\n", "\n", "# ========== 全局配置 ==========\n", - "MODEL_ID = 'ms://Qwen/Qwen3.6-27B'\n", + "MODEL_ID = 'ms://Qwen/Qwen3.8-27B'\n", "NUM_GENERATIONS = 4\n", "MAX_NEW_TOKENS = 1024\n", "LEARNING_RATE = 2e-5\n", @@ -339,7 +339,7 @@ "source": [ "# 初始化 Twinkle 客户端\n", "client = init_twinkle_client(\n", - " base_url='http://www.modelscope.cn/twinkle',\n", + " base_url='https://www.modelscope.cn/twinkle',\n", " api_key=API_KEY,\n", ")\n", "\n", @@ -532,7 +532,7 @@ "from twinkle_client.model import MultiLoraTransformersModel\n", "# 步骤 1:初始化 Twinkle 客户端。\n", "# Tinker 检查点(twinkle:// 路径)由同一检查点服务解析\n", - "init_twinkle_client(base_url='http://www.modelscope.cn/twinkle', api_key=API_KEY)\n", + "init_twinkle_client(base_url='https://www.modelscope.cn/twinkle', api_key=API_KEY)\n", "\n", "# 步骤 2:创建模型客户端(上传时无需训练状态)\n", "model = MultiLoraTransformersModel(model_id=MODEL_ID)\n", @@ -579,7 +579,7 @@ "\n", "logger = get_logger()\n", "\n", - "BASE_MODEL = 'Qwen/Qwen3.6-27B'\n", + "BASE_MODEL = 'Qwen/Qwen3.8-27B'\n", "\n", "# TODO: 替换为训练输出的检查点路径\n", "weight_path = '<替换为你的 twinkle:// 检查点路径>' # 例如: save_result.path\n", @@ -588,7 +588,7 @@ "from tinker import ServiceClient\n", "\n", "service_client = ServiceClient(\n", - " base_url='http://www.modelscope.cn/twinkle',\n", + " base_url='https://www.modelscope.cn/twinkle',\n", " api_key=API_KEY,\n", ")\n", "\n", @@ -646,7 +646,7 @@ "from peft import PeftModel\n", "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "\n", - "base_model_id = 'Qwen/Qwen3.6-27B'\n", + "base_model_id = 'Qwen/Qwen3.8-27B'\n", "lora_path = '<替换为你的 LoRA 检查点路径>'\n", "output_dir = '<替换为输出目录>'\n", "\n", @@ -678,7 +678,7 @@ "vllm serve <输出目录> --tensor-parallel-size 2\n", "```\n", "\n", - "> **提示**:对于 Dense 模型(如 Qwen3.6-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" + "> **提示**:对于 Dense 模型(如 Qwen3.8-27B),LoRA 权重可以直接通过 vLLM 的 `enable_lora` 加载,无需合并。只有在不支持动态 LoRA 的部署场景下才需要合并。\n" ] }, { diff --git a/src/twinkle/server/utils/template_utils.py b/src/twinkle/server/utils/template_utils.py index c593a8c07..839242521 100644 --- a/src/twinkle/server/utils/template_utils.py +++ b/src/twinkle/server/utils/template_utils.py @@ -8,9 +8,14 @@ # Template mapping for different model families # Key: model name pattern to match, Value: template name +# Qwen3.6 / Qwen3.8 share Qwen3.5's architecture (``Qwen3_5ForConditionalGeneration``, +# ``model_type: qwen3_5``) and therefore its template. Qwen3.8 differs from Qwen3.6 +# only in chat_template semantics (``reasoning_effort``, ``preserve_thinking`` default), +# which the jinja template itself handles. MODEL_TEMPLATE_MAPPING = { 'Qwen3.5': 'Qwen3_5Template', 'Qwen3.6': 'Qwen3_5Template', + 'Qwen3.8': 'Qwen3_5Template', # Add more model-template mappings here as needed # 'ModelName': 'TemplateName', } diff --git a/tests/template/test_qwen3_5_template_consistency.py b/tests/template/test_qwen3_5_template_consistency.py new file mode 100644 index 000000000..d479d53a5 --- /dev/null +++ b/tests/template/test_qwen3_5_template_consistency.py @@ -0,0 +1,369 @@ +# Copyright (c) ModelScope Contributors. All rights reserved. +"""Tests for the Qwen3.5-family template registration and encode consistency. + +Two layers: + + - ``TestModelTemplateMapping`` is a pure unit test over + ``MODEL_TEMPLATE_MAPPING``; no model files needed, always runs. + - ``TestEncodeMatchesApplyChatTemplate`` asserts that + ``Template.encode()`` reproduces ``processor.apply_chat_template()`` + byte-for-byte. It is parametrized over whichever Qwen3.5-family models + are present locally (Qwen3.5 / Qwen3.6 / Qwen3.8 all report + ``model_type: qwen3_5*``), and skips when none are available. + +Why the consistency layer matters: ``encode`` routes through +``apply_chat_template`` but first applies ``_to_standard_reasoning_content`` +(inline ```` -> ``reasoning_content`` field) and the Qwen3 template +patches. Any of those can silently desynchronize the rendered text from the +token stream, which breaks multi-turn rollout bridge alignment. Qwen3.8 makes +this concrete: it dropped the template's inline ```` parsing and +flipped the ``preserve_thinking`` default to true, so feeding raw inline +thinking straight to ``apply_chat_template`` yields a doubly-nested +```` block. ``test_inline_thinking_is_normalized_not_double_wrapped`` +pins down that twinkle's normalization prevents exactly that. + +Point the tests at specific checkouts with +``TWINKLE_TEST_QWEN35_MODEL_DIRS=/path/a:/path/b``. +""" +import json +import os +import pytest +import re +from pathlib import Path +from typing import List, Tuple + +import twinkle +from twinkle.data_format import Message, Trajectory +from twinkle.server.utils.template_utils import DEFAULT_TEMPLATE, get_template_for_model + +twinkle.initialize(mode='local') + +_QWEN3_5_TEMPLATE = 'Qwen3_5Template' + +# --------------------------------------------------------------------------- +# Part A: model name -> template mapping (no model files required) +# --------------------------------------------------------------------------- + + +class TestModelTemplateMapping: + """Qwen3.5 / 3.6 / 3.8 share one architecture, hence one template.""" + + @pytest.mark.parametrize( + 'model_name', + [ + 'Qwen3.5-4B', + 'Qwen3.5-35B-A3B', + 'ms://Qwen/Qwen3.5-9B', + 'Qwen3.6-27B', + 'Qwen3.6-35B-A3B', + 'ms://Qwen/Qwen3.6-27B', + # Qwen3.8 ships under a preview org and carries a date suffix; + # both must still resolve through the 'Qwen3.8' substring. + 'Qwen3.8-27B', + 'Qwen3.8-27B-0811', + 'QM-preview/Qwen3.8-27B-0811', + 'ms://QM-preview/Qwen3.8-27B-0811', + ], + ) + def test_qwen3_5_family_maps_to_qwen3_5_template(self, model_name): + assert get_template_for_model(model_name) == _QWEN3_5_TEMPLATE + + @pytest.mark.parametrize('model_name', [ + 'Qwen2.5-7B-Instruct', + 'Qwen3-8B', + 'llama-3-8b', + 'DeepSeek-V4-Flash', + '', + ]) + def test_non_family_models_fall_back_to_default(self, model_name): + assert get_template_for_model(model_name) == DEFAULT_TEMPLATE + + def test_qwen3_8_is_registered(self): + """Regression guard: Qwen3.8 must not silently fall back to Template. + + Without the mapping entry the default template renders a different + prompt, which is a near-invisible training bug. + """ + assert get_template_for_model('Qwen3.8-27B-0811') != DEFAULT_TEMPLATE + + +# --------------------------------------------------------------------------- +# Local model discovery +# --------------------------------------------------------------------------- + + +def _config_dir(root: Path) -> Path: + """Resolve a model root to the directory holding config.json.""" + if (root / 'config.json').is_file(): + return root + for candidate in sorted(root.glob('snapshots/*')): + if (candidate / 'config.json').is_file(): + return candidate + return root + + +def _is_usable(cfg_dir: Path) -> bool: + """Require a Qwen3.5-family config *and* a chat template to render with.""" + cfg_path = cfg_dir / 'config.json' + if not cfg_path.is_file(): + return False + try: + model_type = json.loads(cfg_path.read_text()).get('model_type', '') + except (json.JSONDecodeError, OSError): + return False + if not str(model_type).startswith('qwen3_5'): + return False + if (cfg_dir / 'chat_template.jinja').is_file(): + return True + tok_cfg = cfg_dir / 'tokenizer_config.json' + try: + return bool(json.loads(tok_cfg.read_text()).get('chat_template')) + except (json.JSONDecodeError, OSError): + return False + + +def _cache_roots() -> List[Path]: + """Candidate ModelScope cache roots, covering both on-disk layouts. + + ``MODELSCOPE_CACHE`` may or may not already include the ``hub`` segment, + and models live under ``/models``, so probe each combination. + """ + bases = [] + env_cache = os.getenv('MODELSCOPE_CACHE') + if env_cache: + bases.append(Path(env_cache)) + bases += [Path.home() / '.cache' / 'modelscope', Path('/root/.cache/modelscope')] + + roots, seen = [], set() + for base in bases: + for models_dir in (base / 'models', base / 'hub' / 'models'): + resolved = str(models_dir) + if resolved not in seen and models_dir.is_dir(): + seen.add(resolved) + roots.append(models_dir) + return roots + + +# Official releases only: finetuned derivatives share the base template and +# would just slow the suite down. The 3.8 preview carries a date suffix, so +# match on the family prefix rather than pinning exact names. +_OFFICIAL_DIR = re.compile(r'^(Qwen|QM-preview)--Qwen3\.[568](-|$)') + + +def _discover_models() -> List[Tuple[str, str]]: + """Return ``(label, path)`` for every local Qwen3.5-family model.""" + override = os.getenv('TWINKLE_TEST_QWEN35_MODEL_DIRS') + if override: + roots = [Path(p) for p in override.split(':') if p] + else: + roots = [p for models_dir in _cache_roots() for p in models_dir.iterdir() if _OFFICIAL_DIR.match(p.name)] + + found, seen = [], set() + for root in sorted(roots, key=lambda p: p.name): + cfg_dir = _config_dir(root) + if root.name not in seen and _is_usable(cfg_dir): + seen.add(root.name) + found.append((root.name, str(cfg_dir))) + return found + + +_MODELS = _discover_models() +_MODEL_IDS = [label for label, _ in _MODELS] + +requires_model = pytest.mark.skipif( + not _MODELS, + reason='No local Qwen3.5-family model found; set TWINKLE_TEST_QWEN35_MODEL_DIRS to run', +) + + +@pytest.fixture(scope='module', params=[path for _, path in _MODELS], ids=_MODEL_IDS) +def template(request): + """A Qwen3_5Template bound to a local checkout (tokenizer/processor only).""" + from twinkle.template import Qwen3_5Template + try: + return Qwen3_5Template(model_id=request.param, max_length=8192) + except Exception as e: # noqa: BLE001 - environment issue, not a test failure + pytest.skip(f'Failed to build template for {request.param}: {e}') + + +# --------------------------------------------------------------------------- +# Part B: encode() must reproduce apply_chat_template() byte-for-byte +# --------------------------------------------------------------------------- + +_PLAIN_CASES = { + 'single_turn': [ + { + 'role': 'user', + 'content': 'What is 1+1?' + }, + { + 'role': 'assistant', + 'content': '2' + }, + ], + 'multi_turn': [ + { + 'role': 'user', + 'content': 'a' + }, + { + 'role': 'assistant', + 'content': 'b' + }, + { + 'role': 'user', + 'content': 'c' + }, + { + 'role': 'assistant', + 'content': 'd' + }, + ], + 'with_system': [ + { + 'role': 'system', + 'content': 'You are a helpful assistant.' + }, + { + 'role': 'user', + 'content': 'hi' + }, + { + 'role': 'assistant', + 'content': 'ok' + }, + ], + 'reasoning_content_field': [ + { + 'role': 'user', + 'content': 'hi' + }, + { + 'role': 'assistant', + 'content': 'ok', + 'reasoning_content': 'let me think' + }, + ], + 'unicode_and_code': [ + { + 'role': 'user', + 'content': '用 Python 写平方函数,你好世界!' + }, + { + 'role': 'assistant', + 'content': '```python\ndef f(x):\n return x**2\n```' + }, + ], +} + +_TOOLS = [{ + 'type': 'function', + 'function': { + 'name': 'get_weather', + 'description': 'Get the weather for a city.', + 'parameters': { + 'type': 'object', + 'properties': { + 'city': { + 'type': 'string' + } + }, + 'required': ['city'], + }, + }, +}] + + +@requires_model +class TestEncodeMatchesApplyChatTemplate: + """``decode(encode(traj)) == apply_chat_template(messages)``.""" + + @staticmethod + def _encoded_text(template, trajectory, **kwargs) -> str: + encoded = template.encode(trajectory, **kwargs) + return template.tokenizer.decode(encoded['input_ids']) + + @staticmethod + def _reference_text(template, messages, tools=None, add_generation_prompt=False) -> str: + """Mirror the kwargs ``Template._apply_chat_template`` passes through.""" + return template.processor.apply_chat_template( + messages, + tools=list(tools or []), + tokenize=False, + add_generation_prompt=add_generation_prompt, + enable_thinking=template.enable_thinking, + ) + + @pytest.mark.parametrize('case', sorted(_PLAIN_CASES), ids=sorted(_PLAIN_CASES)) + def test_encode_matches_reference(self, template, case): + messages = _PLAIN_CASES[case] + trajectory = Trajectory(messages=[Message(**m) for m in messages]) + assert self._encoded_text(template, trajectory) == self._reference_text(template, messages) + + def test_encode_matches_reference_with_generation_prompt(self, template): + messages = [{'role': 'user', 'content': 'What is 1+1?'}] + trajectory = Trajectory(messages=[Message(**m) for m in messages]) + got = self._encoded_text(template, trajectory, add_generation_prompt=True) + expected = self._reference_text(template, messages, add_generation_prompt=True) + assert got == expected + + def test_encode_matches_reference_with_tools(self, template): + messages = [ + { + 'role': 'user', + 'content': 'Weather in Beijing?' + }, + { + 'role': 'assistant', + 'content': 'Let me check.' + }, + ] + trajectory = Trajectory(messages=[Message(**m) for m in messages], tools=_TOOLS) + got = self._encoded_text(template, trajectory) + expected = self._reference_text(template, messages, tools=_TOOLS) + assert got == expected + + def test_inline_thinking_is_normalized_not_double_wrapped(self, template): + """Inline ```` must be hoisted into ``reasoning_content``. + + On Qwen3.8 the template no longer parses inline ```` itself, so + passing raw inline thinking to ``apply_chat_template`` produces a + doubly-nested ```` block. ``encode`` must instead match the + explicitly-normalized message list. + """ + inline = [ + { + 'role': 'user', + 'content': 'hi' + }, + { + 'role': 'assistant', + 'content': '\nreason step\n\n\nanswer' + }, + ] + normalized = [ + { + 'role': 'user', + 'content': 'hi' + }, + { + 'role': 'assistant', + 'content': 'answer', + 'reasoning_content': 'reason step' + }, + ] + trajectory = Trajectory(messages=[Message(**m) for m in inline]) + got = self._encoded_text(template, trajectory) + + assert got == self._reference_text(template, normalized) + # The exact corruption this guards against. + assert '\n\n\n\n' not in got + assert got.count('') == got.count('') + + def test_labels_align_with_input_ids(self, template): + """Sanity: the assistant span is supervised, the prompt span is not.""" + trajectory = Trajectory(messages=[Message(**m) for m in _PLAIN_CASES['single_turn']]) + encoded = template.encode(trajectory) + assert len(encoded['labels']) == len(encoded['input_ids']) + assert (encoded['labels'] == -100).sum() > 0 + assert (encoded['labels'] != -100).sum() > 0