diff --git a/INSTALL_CLIENT.ps1 b/INSTALL_CLIENT.ps1 new file mode 100644 index 00000000..83846259 --- /dev/null +++ b/INSTALL_CLIENT.ps1 @@ -0,0 +1,315 @@ +# ============================================================================== +# Twinkle Client Installation Script for Windows +# +# This script sets up a Python environment for using Twinkle client with Tinker. +# It will: +# 1. Check if conda is installed; if not, download and install Miniconda +# 2. Create a new conda environment with Python 3.11 +# 3. Install twinkle-kit with tinker dependencies +# +# Usage (run in PowerShell): +# .\INSTALL_CLIENT.ps1 [ENV_NAME] +# +# Arguments: +# ENV_NAME - Name of the conda environment (default: twinkle-client) +# +# After installation, activate the environment with: +# conda activate twinkle-client +# ============================================================================== + +param( + [string]$EnvName = "twinkle-client" +) + +$ErrorActionPreference = "Stop" +$PythonVersion = "3.11" +$MinicondaUrl = "https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe" + +Write-Host "==========================================" -ForegroundColor Cyan +Write-Host "Twinkle Client Installation (Windows)" -ForegroundColor Cyan +Write-Host "==========================================" -ForegroundColor Cyan +Write-Host "Environment name: $EnvName" +Write-Host "Python version: $PythonVersion" +Write-Host "" + +# ============================================================================== +# Step 1: Check and install Conda +# ============================================================================== + +function Test-CondaInstalled { + try { + $condaVersion = conda --version 2>$null + if ($condaVersion) { + Write-Host "[OK] Conda is already installed: $condaVersion" -ForegroundColor Green + return $true + } + } catch {} + + # Check common installation paths + $condaPaths = @( + "$env:USERPROFILE\miniconda3\Scripts\conda.exe", + "$env:USERPROFILE\anaconda3\Scripts\conda.exe", + "C:\ProgramData\miniconda3\Scripts\conda.exe", + "C:\ProgramData\Anaconda3\Scripts\conda.exe" + ) + + foreach ($path in $condaPaths) { + if (Test-Path $path) { + $condaDir = Split-Path (Split-Path $path) + Write-Host "[!] Found conda at: $condaDir" -ForegroundColor Yellow + Write-Host " Adding to PATH for this session..." + $env:PATH = "$condaDir\Scripts;$condaDir;$env:PATH" + return $true + } + } + + Write-Host "[!] Conda not found" -ForegroundColor Yellow + return $false +} + +function Install-Miniconda { + Write-Host "" + Write-Host "Installing Miniconda..." -ForegroundColor Cyan + + $installerPath = "$env:TEMP\Miniconda3-latest-Windows-x86_64.exe" + $installDir = "$env:USERPROFILE\miniconda3" + + Write-Host "Downloading Miniconda from: $MinicondaUrl" + + # Download installer + try { + Invoke-WebRequest -Uri $MinicondaUrl -OutFile $installerPath -UseBasicParsing + } catch { + Write-Host "[ERROR] Failed to download Miniconda: $_" -ForegroundColor Red + exit 1 + } + + Write-Host "Installing Miniconda to: $installDir" + Write-Host "This may take a few minutes..." + + # Run installer silently + Start-Process -FilePath $installerPath -ArgumentList @( + "/InstallationType=JustMe", + "/RegisterPython=0", + "/AddToPath=1", + "/S", + "/D=$installDir" + ) -Wait -NoNewWindow + + # Add to PATH for current session + $env:PATH = "$installDir\Scripts;$installDir;$env:PATH" + + # Clean up + Remove-Item $installerPath -Force -ErrorAction SilentlyContinue + + Write-Host "[OK] Miniconda installed successfully" -ForegroundColor Green + Write-Host "" + Write-Host "[!] IMPORTANT: Restart PowerShell after installation to use conda globally" -ForegroundColor Yellow +} + +if (-not (Test-CondaInstalled)) { + $response = Read-Host "Do you want to install Miniconda? [Y/n]" + if ($response -match "^[Nn]") { + Write-Host "Installation cancelled. Please install conda manually." + exit 1 + } + Install-Miniconda +} + +# Initialize conda for PowerShell +try { + $condaHook = conda shell.powershell hook 2>$null | Out-String + if ($condaHook) { + Invoke-Expression $condaHook + } +} catch {} + +# ============================================================================== +# Step 2: Create conda environment +# ============================================================================== + +Write-Host "" +Write-Host "Creating conda environment: $EnvName (Python $PythonVersion)..." -ForegroundColor Cyan + +# Accept Conda ToS for default channels (required for conda >= 26.x) +Write-Host "Accepting Conda Terms of Service for default channels..." +try { + conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main 2>$null + conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r 2>$null + conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/msys2 2>$null +} catch { + # Older conda versions don't have tos command, ignore +} + +# Check if environment already exists +$envList = conda env list 2>$null +if ($envList -match "^$EnvName\s") { + Write-Host "[!] Environment '$EnvName' already exists." -ForegroundColor Yellow + $response = Read-Host "Do you want to remove and recreate it? [y/N]" + if ($response -match "^[Yy]") { + Write-Host "Removing existing environment..." + conda env remove -n $EnvName -y + } else { + Write-Host "Using existing environment..." + } +} + +# Create environment if it doesn't exist +$envList = conda env list 2>$null +if (-not ($envList -match "^$EnvName\s")) { + Write-Host "Running: conda create -n $EnvName python=$PythonVersion -y" + conda create -n $EnvName python=$PythonVersion -y + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to create conda environment. Exit code: $LASTEXITCODE" -ForegroundColor Red + exit 1 + } +} + +# Verify environment was created +$envList = conda env list 2>$null +if (-not ($envList -match "^$EnvName\s")) { + Write-Host "[ERROR] Environment '$EnvName' was not created successfully." -ForegroundColor Red + Write-Host "Please run manually:" -ForegroundColor Yellow + Write-Host " conda create -n $EnvName python=$PythonVersion" -ForegroundColor Yellow + exit 1 +} + +Write-Host "[OK] Environment '$EnvName' is ready" -ForegroundColor Green + +# ============================================================================== +# Step 3: Install dependencies +# ============================================================================== + +Write-Host "" +Write-Host "Activating environment and installing dependencies..." -ForegroundColor Cyan + +# Activate environment +Write-Host "Activating environment '$EnvName'..." +try { + conda activate $EnvName + if ($LASTEXITCODE -ne 0) { + throw "conda activate failed" + } +} catch { + Write-Host "[!] Standard activation failed, trying alternative method..." -ForegroundColor Yellow + # Alternative: run commands in the conda environment directly + $condaBase = (conda info --base 2>$null).Trim() + $activateScript = Join-Path $condaBase "Scripts\activate.bat" + if (Test-Path $activateScript) { + cmd /c "call `"$activateScript`" $EnvName && pip --version" 2>$null + } +} + +# Verify we're in the correct environment +$currentPython = python --version 2>&1 +Write-Host "Current Python: $currentPython" +if ($currentPython -notmatch "3\.11") { + Write-Host "[!] Warning: Python version mismatch. Expected 3.11, got: $currentPython" -ForegroundColor Yellow + Write-Host "Attempting to use conda run instead..." -ForegroundColor Yellow + $UseCondaRun = $true +} else { + $UseCondaRun = $false +} + +# Upgrade pip +if ($UseCondaRun) { + conda run -n $EnvName pip install --upgrade pip +} else { + pip install --upgrade pip +} + +# Check if we're in the twinkle source directory +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +if (Test-Path "$ScriptDir\pyproject.toml") { + Write-Host "" + Write-Host "Installing twinkle from source (with tinker support)..." -ForegroundColor Cyan + if ($UseCondaRun) { + conda run -n $EnvName pip install -e "$ScriptDir[tinker]" + } else { + pip install -e "$ScriptDir[tinker]" + } +} else { + Write-Host "" + Write-Host "Installing twinkle-kit from PyPI (with tinker support)..." -ForegroundColor Cyan + if ($UseCondaRun) { + conda run -n $EnvName pip install "twinkle-kit[tinker]" + } else { + pip install "twinkle-kit[tinker]" + } +} + +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to install twinkle. Exit code: $LASTEXITCODE" -ForegroundColor Red + exit 1 +} + +# ============================================================================== +# Step 4: Verify installation +# ============================================================================== + +Write-Host "" +Write-Host "Verifying installation..." -ForegroundColor Cyan +Write-Host "" + +$verifyScript = @" +import sys +print(f'Python: {sys.version}') +print() + +packages = [ + 'twinkle', + 'twinkle_client', + 'tinker', + 'transformers', + 'peft', + 'modelscope', + 'datasets', +] + +print('Installed packages:') +print('-' * 40) + +for pkg in packages: + try: + mod = __import__(pkg) + version = getattr(mod, '__version__', 'unknown') + print(f' {pkg}: {version}') + except ImportError as e: + print(f' {pkg}: NOT INSTALLED ({e})') + +print() +print('Testing twinkle client imports...') +try: + from twinkle_client import init_tinker_client + print(' [OK] init_tinker_client available') +except ImportError as e: + print(f' [FAIL] init_tinker_client: {e}') + +try: + from twinkle.dataloader import DataLoader + from twinkle.dataset import Dataset, DatasetMeta + from twinkle.preprocessor import SelfCognitionProcessor + print(' [OK] twinkle core components available') +except ImportError as e: + print(f' [FAIL] twinkle core: {e}') +"@ + +if ($UseCondaRun) { + conda run -n $EnvName python -c $verifyScript +} else { + python -c $verifyScript +} + +Write-Host "" +Write-Host "==========================================" -ForegroundColor Cyan +Write-Host "Installation complete!" -ForegroundColor Green +Write-Host "==========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "To activate the environment, run:" +Write-Host " conda activate $EnvName" -ForegroundColor Yellow +Write-Host "" +Write-Host "Example usage (see cookbook/client/tinker/):" +Write-Host ' $env:MODELSCOPE_TOKEN = "your-token"' -ForegroundColor Yellow +Write-Host " python cookbook/client/tinker/modelscope_service/self_cognition.py" -ForegroundColor Yellow +Write-Host "" diff --git a/INSTALL_CLIENT.sh b/INSTALL_CLIENT.sh new file mode 100644 index 00000000..ecc7e90c --- /dev/null +++ b/INSTALL_CLIENT.sh @@ -0,0 +1,237 @@ +#!/bin/bash +# ============================================================================== +# Twinkle Client Installation Script +# +# This script sets up a Python environment for using Twinkle client with Tinker. +# It will: +# 1. Check if conda is installed; if not, download and install Miniconda +# 2. Create a new conda environment with Python 3.11 +# 3. Install twinkle-kit with tinker dependencies +# +# Usage: +# chmod +x INSTALL_CLIENT.sh +# ./INSTALL_CLIENT.sh [ENV_NAME] +# +# Arguments: +# ENV_NAME - Name of the conda environment (default: twinkle-client) +# +# After installation, activate the environment with: +# conda activate twinkle-client +# ============================================================================== + +set -e # Exit immediately on error + +# Configuration +ENV_NAME="${1:-twinkle-client}" +PYTHON_VERSION="3.11" +MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest" + +echo "==========================================" +echo "Twinkle Client Installation" +echo "==========================================" +echo "Environment name: $ENV_NAME" +echo "Python version: $PYTHON_VERSION" +echo "" + +# ============================================================================== +# Step 1: Check and install Conda +# ============================================================================== + +check_conda() { + if command -v conda &> /dev/null; then + echo "[✓] Conda is already installed: $(conda --version)" + return 0 + else + echo "[!] Conda not found" + return 1 + fi +} + +install_miniconda() { + echo "" + echo "Installing Miniconda..." + + # Detect OS and architecture + OS_TYPE=$(uname -s) + ARCH=$(uname -m) + + case "$OS_TYPE" in + Linux) + INSTALLER_URL="${MINICONDA_URL}-Linux-${ARCH}.sh" + ;; + Darwin) + if [ "$ARCH" = "arm64" ]; then + INSTALLER_URL="${MINICONDA_URL}-MacOSX-arm64.sh" + else + INSTALLER_URL="${MINICONDA_URL}-MacOSX-x86_64.sh" + fi + ;; + *) + echo "[ERROR] Unsupported OS: $OS_TYPE" + echo "Please install Miniconda manually from: https://docs.conda.io/en/latest/miniconda.html" + exit 1 + ;; + esac + + echo "Downloading Miniconda from: $INSTALLER_URL" + + # Download installer + INSTALLER_PATH="/tmp/miniconda_installer.sh" + if command -v curl &> /dev/null; then + curl -fsSL "$INSTALLER_URL" -o "$INSTALLER_PATH" + elif command -v wget &> /dev/null; then + wget -q "$INSTALLER_URL" -O "$INSTALLER_PATH" + else + echo "[ERROR] Neither curl nor wget found. Please install one of them." + exit 1 + fi + + # Run installer + CONDA_INSTALL_DIR="$HOME/miniconda3" + echo "Installing Miniconda to: $CONDA_INSTALL_DIR" + bash "$INSTALLER_PATH" -b -p "$CONDA_INSTALL_DIR" + + # Initialize conda + "$CONDA_INSTALL_DIR/bin/conda" init bash zsh 2>/dev/null || true + + # Add to current session + export PATH="$CONDA_INSTALL_DIR/bin:$PATH" + + # Clean up + rm -f "$INSTALLER_PATH" + + echo "[✓] Miniconda installed successfully" + echo "" + echo "[!] IMPORTANT: Restart your shell or run:" + echo " source ~/.bashrc # or ~/.zshrc" +} + +if ! check_conda; then + read -p "Do you want to install Miniconda? [Y/n] " -n 1 -r + echo + if [[ $REPLY =~ ^[Nn]$ ]]; then + echo "Installation cancelled. Please install conda manually." + exit 1 + fi + install_miniconda +fi + +# Ensure conda command is available +eval "$(conda shell.bash hook 2>/dev/null)" || true + +# ============================================================================== +# Step 2: Create conda environment +# ============================================================================== + +echo "" +echo "Creating conda environment: $ENV_NAME (Python $PYTHON_VERSION)..." + +# Check if environment already exists +if conda env list | grep -q "^${ENV_NAME} "; then + echo "[!] Environment '$ENV_NAME' already exists." + read -p "Do you want to remove and recreate it? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Removing existing environment..." + conda env remove -n "$ENV_NAME" -y + else + echo "Using existing environment..." + fi +fi + +# Create environment if it doesn't exist +if ! conda env list | grep -q "^${ENV_NAME} "; then + conda create -n "$ENV_NAME" python="$PYTHON_VERSION" -y +fi + +echo "[✓] Environment '$ENV_NAME' is ready" + +# ============================================================================== +# Step 3: Install dependencies +# ============================================================================== + +echo "" +echo "Activating environment and installing dependencies..." + +# Activate environment +conda activate "$ENV_NAME" + +# Upgrade pip +pip install --upgrade pip + +# Check if we're in the twinkle source directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [ -f "$SCRIPT_DIR/pyproject.toml" ]; then + echo "" + echo "Installing twinkle from source (with tinker support)..." + pip install -e "$SCRIPT_DIR[tinker]" +else + echo "" + echo "Installing twinkle-kit from PyPI (with tinker support)..." + pip install 'twinkle-kit[tinker]' +fi + +# ============================================================================== +# Step 4: Verify installation +# ============================================================================== + +echo "" +echo "Verifying installation..." +echo "" + +python -c " +import sys +print(f'Python: {sys.version}') +print() + +packages = [ + 'twinkle', + 'twinkle_client', + 'tinker', + 'transformers', + 'peft', + 'modelscope', + 'datasets', +] + +print('Installed packages:') +print('-' * 40) + +for pkg in packages: + try: + mod = __import__(pkg) + version = getattr(mod, '__version__', 'unknown') + print(f' {pkg}: {version}') + except ImportError as e: + print(f' {pkg}: NOT INSTALLED ({e})') + +print() +print('Testing twinkle client imports...') +try: + from twinkle_client import init_tinker_client + print(' [✓] init_tinker_client available') +except ImportError as e: + print(f' [✗] init_tinker_client: {e}') + +try: + from twinkle.dataloader import DataLoader + from twinkle.dataset import Dataset, DatasetMeta + from twinkle.preprocessor import SelfCognitionProcessor + print(' [✓] twinkle core components available') +except ImportError as e: + print(f' [✗] twinkle core: {e}') +" + +echo "" +echo "==========================================" +echo "Installation complete!" +echo "==========================================" +echo "" +echo "To activate the environment, run:" +echo " conda activate $ENV_NAME" +echo "" +echo "Example usage (see cookbook/client/tinker/):" +echo " export MODELSCOPE_TOKEN='your-token'" +echo " python cookbook/client/tinker/modelscope_service/self_cognition.py" +echo "" diff --git a/README.md b/README.md index d0693836..407b0658 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,26 @@ cd twinkle pip install -e . ``` +If you need to use Twinkle's Client, you can use our one-click installation script: + +```shell +# Mac or Linux +sh INSTALL_CLIENT.sh +# Windows, Open with powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +.\INSTALL_CLIENT.ps1 +``` + +This script will download or utilize conda to create a virtual environment called `twinkle-client`, which can be directly used for remote training. + +If you need to install Megatron-related dependencies, you can use the following script: + +```shell +sh INSTALL_MEGATRON.sh +``` + +Or use ModelScope's [official image](https://www.modelscope.cn/docs/intro/environment-setup). + ## Tutorials | Training Type | Model Framework | Cookbook Path | diff --git a/README_ZH.md b/README_ZH.md index 11a6cccc..1bf7eb08 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -53,6 +53,26 @@ cd twinkle pip install -e . ``` +如果你需要使用Twinkle的Client,可以使用我们的一键安装脚本: + +```shell +# Mac or Linux +sh INSTALL_CLIENT.sh +# Windows, Open with powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +.\INSTALL_CLIENT.ps1 +``` + +这个脚本会下载或利用conda,创建一个叫`twinkle-client`的虚拟环境,这个环境可以直接用于远端训练。 + +如果你需要安装Megatron相关依赖,可以如下脚本: + +```shell +sh INSTALL_MEGATRON.sh +``` + +或者使用魔搭的[官方镜像](https://www.modelscope.cn/docs/intro/environment-setup)。 + ## 教程 | 训练类型 | 模型框架 | Cookbook 路径 | diff --git a/docs/source_en/Usage Guide/Quick-Start.md b/docs/source_en/Usage Guide/Quick-Start.md index 24820fea..a7c7c238 100644 --- a/docs/source_en/Usage Guide/Quick-Start.md +++ b/docs/source_en/Usage Guide/Quick-Start.md @@ -28,6 +28,30 @@ Twinkle and [ms-swift](https://github.com/modelscope/ms-swift) are both model tr - If you need other capabilities like inference, deployment, quantization - If you are sensitive to new model training support, Swift guarantees day-0 update capability +## Model Training and Twinkle + +When you find that general-purpose large models cannot meet your needs, training becomes essential: + +- **Make the model know you**: Through self-cognition training, the model can answer questions like "Who are you?" and "Who is your developer?", becoming an AI assistant exclusively yours. +- **Make the model understand your business**: By fine-tuning with private data, the model can learn your industry terminology, business processes, and internal knowledge base, becoming a domain expert. +- **Make the model think your way**: Through reinforcement learning (RL), you can define reward rules to guide the model in generating outputs that match your expected format, reasoning style, or values. +- **Make the model stronger**: Distill capabilities from large models to smaller ones, or inject new knowledge through continued pre-training, enabling the model's capabilities to continuously evolve. + +After training is complete, you can deploy the model to your own servers, publish it to ModelScope/Hugging Face to share with the community, or deploy your service using deployment frameworks like vLLM. + +Existing training frameworks can be roughly divided into three categories: + +- **Low-level frameworks** (e.g., native PyTorch): Highly flexible, but require developers to build infrastructure from scratch including distributed computing, data loading, checkpointing, etc., resulting in high development costs and long cycles. +- **High-level frameworks** (e.g., ms-swift, transformers Trainer): Ready to use out of the box—just provide the dataset and configuration to complete training—but the training process is a black box, making it difficult to customize algorithm details. +- **Heavy-duty frameworks** (e.g., Megatron-LM): Designed for ultra-large-scale models with support for complex parallelism strategies, but have a steep learning curve and highly invasive code requirements. + +Twinkle's design goal is to find a balance among these three types of frameworks: + +1. **Retain control over the training loop**: Developers can clearly see and control every step of forward, backward, and step, making it easy to debug and customize algorithms. +2. **Provide highly cohesive component abstractions**: Components like Dataset, Model, Sampler, and Loss each have their own responsibilities and can be used independently or in combination, without requiring full integration. +3. **Hide distributed complexity**: Whether using a single GPU, torchrun, or a Ray cluster, the training code remains almost identical—only the initialization parameters need to be modified. +4. **Support production-grade deployment**: Built-in capabilities for multi-tenancy, HTTP services, weight synchronization, and more, ready for building enterprise-level training platforms. + ## Usage Patterns ### Using Only Partial Components @@ -742,6 +766,8 @@ if __name__ == '__main__': Concurrent with the open-source release of the Twinkle framework, we also provide a hosted Training as a Service (TaaS) powered by ModelScope's backend services. Developers can experience Twinkle's training API for free through this service. This service shares the same code as the Tinker API section described above. The only difference is that the Endpoint and Token need to use the official ModelScope information. For details on how to use the official service, please refer to the detailed description in [Training Service](./Train-as-a-Service.md). +Twinkle provides a sampling API that can be used to control the sampling process more flexibly for result validation, or to participate in the sampling workflow of RL algorithms. + ## Using Hugging Face models Switch the prefix. 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 fd6c30f3..efe31108 100644 --- a/docs/source_en/Usage Guide/Train-as-a-Service.md +++ b/docs/source_en/Usage Guide/Train-as-a-Service.md @@ -16,7 +16,7 @@ API endpoint: `base_url="https://www.modelscope.cn/twinkle"` ## Step 2. Review the Cookbook and Customize Development -We strongly recommend that developers review our [cookbook](https://github.com/modelscope/twinkle/tree/main/cookbook/client/tinker) and build upon the training code provided there. +We strongly recommend that developers check out our [cookbook](https://github.com/modelscope/twinkle/tree/main/cookbook/client/tinker) and build upon the training code provided there for secondary development. Sample code: @@ -49,7 +49,7 @@ service_client = ServiceClient(base_url=base_url, api_key=api_key) training_client = service_client.create_lora_training_client(base_model=base_model[len('ms://'):], rank=16) # Training loop: use input_feature_to_datum to transfer the input format -for epoch in range(3): +for epoch in range(2): for step, batch in tqdm(enumerate(dataloader)): input_datum = [input_feature_to_datum(input_feature) for input_feature in batch] @@ -58,10 +58,82 @@ for epoch in range(3): fwdbwd_result = fwdbwd_future.result() optim_result = optim_future.result() + print(f'Training Metrics: {optim_result}') - training_client.save_state(f"twinkle-lora-{epoch}").result() + result = training_client.save_state(f"twinkle-lora-{epoch}").result() + 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-30B-A3B-Instruct-2507`. 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 +from tinker import types + +from twinkle.data_format import Message, Trajectory +from twinkle.template import Template +from twinkle import init_tinker_client + +# Step 1: Initialize Tinker client +init_tinker_client() + +from tinker import ServiceClient + +base_model = 'Qwen/Qwen3-30B-A3B-Instruct-2507' +base_url = 'http://www.modelscope.cn/twinkle' + +# Step 2: Define the base model and connect to the server +service_client = ServiceClient( + base_url=base_url, + api_key=os.environ.get('MODELSCOPE_TOKEN') +) + +# Step 3: Create a sampling client by loading weights from a saved checkpoint. +# 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-30B-A3B-Instruct-2507-xxx/weights/twinkle-lora-1', + base_model=base_model +) + +# Step 4: Load the tokenizer locally to encode the prompt and decode the results +print(f'Using model {base_model}') + +template = Template(model_id=f'ms://{base_model}') + +trajectory = Trajectory( + messages=[ + Message(role='system', content='You are a helpful assistant'), + Message(role='user', content='Who are you?'), + ] +) + +input_feature = template.encode(trajectory, add_generation_prompt=True) + +input_ids = input_feature['input_ids'].tolist() + +# Step 5: Prepare the prompt and sampling parameters +prompt = types.ModelInput.from_ints(input_ids) +params = types.SamplingParams( + max_tokens=128, # Maximum number of tokens to generate + temperature=0.7, + stop=['\n'] # Stop generation when a newline character is produced +) + +# Step 6: Send the sampling request to the server. +# num_samples=1 generates 1 independent completions for the same prompt. +print('Sampling...') +future = sampling_client.sample(prompt=prompt, sampling_params=params, num_samples=1) +result = future.result() + +# Step 7: Decode and print the generated responses +print('Responses:') +for i, seq in enumerate(result.sequences): + print(f'{i}: {repr(template.decode(seq.tokens))}') +``` + +Developers can also merge this LoRA with the base model and then deploy it using their own service, calling it through the OpenAI-compatible standard API. + > The ModelScope server is tinker-compatible, so use the tinker cookbooks. In the future version, we will support a server works both for twinkle/tinker clients. Developers can customize datasets, advantage functions, rewards, templates, and more. However, the Loss component is not currently customizable since it needs to be executed on the server side (for security reasons). If you need support for additional Loss functions, you can upload your Loss implementation to ModelHub and contact us via the Q&A group or through an issue to have the corresponding component added to the whitelist. diff --git "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\345\277\253\351\200\237\345\274\200\345\247\213.md" "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\345\277\253\351\200\237\345\274\200\345\247\213.md" index 5e4cbf0d..162291a6 100644 --- "a/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\345\277\253\351\200\237\345\274\200\345\247\213.md" +++ "b/docs/source_zh/\344\275\277\347\224\250\346\214\207\345\274\225/\345\277\253\351\200\237\345\274\200\345\247\213.md" @@ -28,6 +28,30 @@ Twinkle 和 [ms-swift](https://github.com/modelscope/ms-swift) 都是模型训 - 如果你需要推理、部署、量化等其他能力 - 如果你对新模型的训练支持敏感,Swift 会保证 day-0 的更新能力 +## 模型训练与Twinkle + +当你发现通用大模型无法满足你的需求时,训练就成为必选项: + +- **让模型认识你**:通过自我认知训练,模型可以回答"你是谁"、"你的开发者是谁"等问题,成为专属于你的 AI 助手。 +- **让模型懂你的业务**:使用私有数据微调,模型可以学会你的行业术语、业务流程、内部知识库,成为领域专家。 +- **让模型按你的方式思考**:通过强化学习(RL),你可以定义奖励规则,引导模型生成符合你期望的输出格式、推理风格或价值观。 +- **让模型更强**:蒸馏大模型的能力到小模型,或通过持续预训练注入新知识,让模型能力持续进化。 + +训练完成后,你可以将模型部署到自己的服务器,或发布到 ModelScope/Hugging Face 与社区分享,或者通过vLLM等部署架构部署你的服务进行使用。 + +现有的训练框架可以大致分为三类: + +- **底层框架**(如原生 PyTorch):灵活性极高,但需要开发者从零搭建分布式、数据加载、checkpoint 等基础设施,开发成本高、周期长。 +- **高层框架**(如 ms-swift、transformers Trainer):开箱即用,只需提供数据集和配置即可完成训练,但训练过程是黑盒,难以定制算法细节。 +- **重型框架**(如 Megatron-LM):为超大规模模型设计,支持复杂的并行策略,但学习曲线陡峭,代码侵入性强。 + +Twinkle 的设计目标是在这三类框架之间找到平衡点: + +1. **保留 training loop 的控制权**:开发者可以清晰看到并控制 forward、backward、step 的每一步,便于调试和定制算法。 +2. **提供高内聚的组件抽象**:Dataset、Model、Sampler、Loss 等组件各司其职,可独立使用也可组合使用,无需整体接入。 +3. **屏蔽分布式复杂性**:无论是单卡、torchrun 还是 Ray 集群,训练代码几乎相同,只需修改初始化参数。 +4. **支持生产级部署**:内置多租户、HTTP 服务、权重同步等能力,可直接用于构建企业级训练平台。 + ## 使用模式 ### 仅使用部分组件 @@ -744,6 +768,8 @@ if __name__ == '__main__': 在 Twinkle 框架开源的同时,我们依托ModelScope的后台服务,也提供了托管的模型训练服务(Training as a Service),开发者可以通过这一服务, 免费体验Twinkle的训练API。 该服务和上面叙述的Tinker API部分代码是相同的,唯一不同的是Endpoint和Token需要使用魔搭官方的对应信息。关于如何使用官方服务,请查看[训练服务](./训练服务.md)的详细描述。 +Twinkle提供了采样API,该API可以用于更灵活地控制采样方式以验证结果,或者参与到RL算法的采样流程中。 + ## 使用Hugging Face的模型 切换前缀即可。 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 c0d5b68f..404315b4 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" @@ -52,7 +52,7 @@ service_client = ServiceClient(base_url=base_url, api_key=api_key) training_client = service_client.create_lora_training_client(base_model=base_model[len('ms://'):], rank=16) # Training loop: use input_feature_to_datum to transfer the input format -for epoch in range(3): +for epoch in range(2): for step, batch in tqdm(enumerate(dataloader)): input_datum = [input_feature_to_datum(input_feature) for input_feature in batch] @@ -61,10 +61,82 @@ for epoch in range(3): fwdbwd_result = fwdbwd_future.result() optim_result = optim_future.result() + print(f'Training Metrics: {optim_result}') - training_client.save_state(f"twinkle-lora-{epoch}").result() + result = training_client.save_state(f"twinkle-lora-{epoch}").result() + print(f'Saved checkpoint for epoch {epoch} to {result.path}') ``` +通过上述代码,你可以训练一个原模型为`Qwen/Qwen3-30B-A3B-Instruct-2507`的自我认知lora。这个lora会改变模型的名称和制造者为训练时指定的名称。使用这个lora进行推理: + +```python +import os +from tinker import types + +from twinkle.data_format import Message, Trajectory +from twinkle.template import Template +from twinkle import init_tinker_client + +# Step 1: Initialize Tinker client +init_tinker_client() + +from tinker import ServiceClient + +base_model = 'Qwen/Qwen3-30B-A3B-Instruct-2507' +base_url = 'http://www.modelscope.cn/twinkle' + +# Step 2: Define the base model and connect to the server +service_client = ServiceClient( + base_url=base_url, + api_key=os.environ.get('MODELSCOPE_TOKEN') +) + +# Step 3: Create a sampling client by loading weights from a saved checkpoint. +# 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-30B-A3B-Instruct-2507-xxx/weights/twinkle-lora-1', + base_model=base_model +) + +# Step 4: Load the tokenizer locally to encode the prompt and decode the results +print(f'Using model {base_model}') + +template = Template(model_id=f'ms://{base_model}') + +trajectory = Trajectory( + messages=[ + Message(role='system', content='You are a helpful assistant'), + Message(role='user', content='Who are you?'), + ] +) + +input_feature = template.encode(trajectory, add_generation_prompt=True) + +input_ids = input_feature['input_ids'].tolist() + +# Step 5: Prepare the prompt and sampling parameters +prompt = types.ModelInput.from_ints(input_ids) +params = types.SamplingParams( + max_tokens=128, # Maximum number of tokens to generate + temperature=0.7, + stop=['\n'] # Stop generation when a newline character is produced +) + +# Step 6: Send the sampling request to the server. +# num_samples=1 generates 1 independent completions for the same prompt. +print('Sampling...') +future = sampling_client.sample(prompt=prompt, sampling_params=params, num_samples=1) +result = future.result() + +# Step 7: Decode and print the generated responses +print('Responses:') +for i, seq in enumerate(result.sequences): + print(f'{i}: {repr(template.decode(seq.tokens))}') +``` + +开发者也可以将这个lora和原模型合并之后,使用自己的服务进行部署,并使用OpenAI标准接口进行调用。 + > 目前的服务兼容tinker client,因此请使用tinker的cookbook进行训练。后续我们会支持单服务器支持twinkle/tinker双client。 开发者可以定制数据集/优势函数/奖励/模板等,其中 Loss 部分由于需要在服务端执行,因此当前暂不支持(安全性原因)。 diff --git a/poetry.lock b/poetry.lock index 60be1051..773c9b14 100644 --- a/poetry.lock +++ b/poetry.lock @@ -264,6 +264,35 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "aliyun-python-sdk-core" +version = "2.11.5" +description = "The core module of Aliyun Python SDK." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "aliyun-python-sdk-core-2.11.5.tar.gz", hash = "sha256:577265c630c02207c692ca19958bd21665d56208306a834d0885e7770553975e"}, +] + +[package.dependencies] +pycryptodome = ">=3.4.7" + +[[package]] +name = "aliyun-python-sdk-kms" +version = "2.16.5" +description = "The kms module of Aliyun Python sdk." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3"}, + {file = "aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0"}, +] + +[package.dependencies] +aliyun-python-sdk-core = ">=2.11.5" + [[package]] name = "annotated-doc" version = "0.0.4" @@ -987,6 +1016,17 @@ transformers = "*" accelerate = ["accelerate"] dev = ["black (==22.12.0)", "flake8 (>=3.8.3)", "isort (==5.8.0)", "nbconvert (>=7.16.3)", "pytest (>=6.0.0)", "wheel (>=0.36.2)"] +[[package]] +name = "crcmod" +version = "1.7" +description = "CRC Generator" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e"}, +] + [[package]] name = "cryptography" version = "46.0.5" @@ -4403,6 +4443,25 @@ files = [ opentelemetry-api = "1.39.1" typing-extensions = ">=4.5.0" +[[package]] +name = "oss2" +version = "2.13.1" +description = "Aliyun OSS (Object Storage Service) SDK" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "oss2-2.13.1.tar.gz", hash = "sha256:8548ea7d43326f6fd679bc8b79b3a2dfbfe9c6a60ed57e2410818fec57023dda"}, +] + +[package.dependencies] +aliyun-python-sdk-core = ">=2.6.2" +aliyun-python-sdk-kms = ">=2.4.1" +crcmod = ">=1.7" +pycryptodome = ">=3.4.7" +requests = "!=2.9.0" +six = "*" + [[package]] name = "outlines-core" version = "0.2.11" @@ -5345,6 +5404,57 @@ files = [ {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] +[[package]] +name = "pycryptodome" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -8966,4 +9076,4 @@ vllm = ["vllm"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "72efad08793bb5308ce6bc5549fb9c4e8d96bf6817c3130f5ab2f9b47ea6e045" +content-hash = "6bc839d412edaa773717488aaf25cb1e5b663f305e3f9945d9f92fe1524160d2" diff --git a/pyproject.toml b/pyproject.toml index bb2873af..0e9640c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,14 +6,15 @@ readme = "README.md" authors = [{ name = "ModelScope", email = "contact@modelscope.cn" }] requires-python = ">=3.11,<3.13" dependencies = [ - "datasets>=3.0,<4.0", "numpy>=2.0.0,<2.3.0", + "datasets>=3.0,<4.0", "omegaconf>=2.3.0,<3.0.0", "fastapi", "modelscope[framework]>=1.34.0", "safetensors", "peft>=0.11.0,<=0.19.0", "transformers", + "oss2", ] [project.optional-dependencies] diff --git a/src/twinkle/utils/parallel.py b/src/twinkle/utils/parallel.py index ee91072d..6dde4871 100644 --- a/src/twinkle/utils/parallel.py +++ b/src/twinkle/utils/parallel.py @@ -1,11 +1,21 @@ # Copyright (c) ModelScope Contributors. All rights reserved. import os +import re from contextlib import contextmanager from datasets.utils.filelock import FileLock os.makedirs('.locks', exist_ok=True) +def _sanitize_lock_name(name: str) -> str: + r"""Sanitize lock file name for cross-platform compatibility. + + Windows does not allow : / \ * ? " < > | in file names. + """ + # Replace problematic characters with underscores + return re.sub(r'[:/\\*?"<>|]', '_', name) + + def acquire_lock(lock: FileLock, blocking: bool): try: lock.acquire(blocking=blocking) @@ -37,7 +47,8 @@ def processing_lock(lock_file: str): Returns: """ - lock: FileLock = FileLock(os.path.join('.locks', f'{lock_file}.lock')) # noqa + lock_name = _sanitize_lock_name(lock_file) + lock: FileLock = FileLock(os.path.join('.locks', f'{lock_name}.lock')) # noqa if acquire_lock(lock, False): try: