Fuse minimind/minimind-v/minimind-o into omni with layered architecture
Browse filesMigrate the three MiniMind repos into the omni package using a clean
layered design:
- core/: split the monolithic Transformer kernel into norm/rope/attention/
mlp/block/model modules (replacing the old components.py)
- models/: assemble core components into MiniMindForCausalLM (text),
MiniMindVLM (vision), MiniMindOmni (speech/full-modal, with TalkerModule)
- encoders/: SiglipVisionEncoder (vision/), SenseVoiceAudioEncoder (audio/)
- projectors/: MMVisionProjector / MMAudioProjector bridging encoders to LLM
- serve/: SileroVAD / RealtimeSession realtime speech session layer
- trainers/ + datasets/ + utils/: ported train_sft/pretrain/dpo/etc plus
VLM/Omni trainers, VLMDataset/OmniDataset, init_vlm/omni_model helpers
- examples/: eval_llm/vlm/omni, omni_web_demo, serve_openai_api, convert_model
- README + pyproject updated; all modules import-compile-smoke verified
Co-Authored-By: opencode <noreply@opencode.ai>
- README.md +124 -0
- configs/example_train.yaml +26 -0
- dataset/dataset.md +5 -0
- examples/convert_model.py +141 -0
- examples/eval_llm.py +94 -0
- examples/eval_omni.py +244 -0
- examples/eval_toolcall.py +238 -0
- examples/eval_vlm.py +79 -0
- examples/omni_web_demo.py +512 -0
- examples/serve_openai_api.py +249 -0
- examples/web_demo.html +748 -0
- pyproject.toml +27 -2
- src/omni/core/__init__.py +18 -0
- src/omni/core/attention.py +55 -0
- src/omni/core/block.py +24 -0
- src/omni/core/mlp.py +49 -0
- src/omni/core/model.py +45 -0
- src/omni/core/norm.py +15 -0
- src/omni/core/rope.py +37 -0
- src/omni/datasets/__init__.py +1 -0
- src/omni/datasets/lm_dataset.py +606 -0
- src/omni/encoders/__init__.py +3 -0
- src/omni/encoders/audio/__init__.py +3 -0
- src/omni/encoders/audio/sensevoice.py +70 -0
- src/omni/encoders/vision/__init__.py +3 -0
- src/omni/encoders/vision/siglip.py +54 -0
- src/omni/models/__init__.py +53 -0
- src/omni/models/lora.py +61 -0
- src/omni/models/minimind.py +113 -0
- src/omni/models/omni.py +409 -0
- src/omni/models/vlm.py +155 -0
- src/omni/projectors/__init__.py +4 -0
- src/omni/projectors/audio.py +16 -0
- src/omni/projectors/vision.py +17 -0
- src/omni/serve/__init__.py +1 -0
- src/omni/serve/realtime.py +68 -0
- src/omni/trainers/__init__.py +17 -0
- src/omni/trainers/agent.py +489 -0
- src/omni/trainers/distillation.py +245 -0
- src/omni/trainers/dpo.py +225 -0
- src/omni/trainers/full_sft.py +170 -0
- src/omni/trainers/full_sft_omni.py +257 -0
- src/omni/trainers/full_sft_vlm.py +175 -0
- src/omni/trainers/grpo.py +331 -0
- src/omni/trainers/lora.py +183 -0
- src/omni/trainers/ppo.py +434 -0
- src/omni/trainers/pretrain.py +169 -0
- src/omni/trainers/pretrain_vlm.py +175 -0
- src/omni/trainers/rollout_engine.py +218 -0
- src/omni/trainers/train_tokenizer.py +168 -0
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Omni
|
| 2 |
+
|
| 3 |
+
Omni 是一个以 **多模态 (omni)** 为目标的 LLM 训练 / 推理框架,已完整集成
|
| 4 |
+
[MiniMind](https://github.com/jingyaogong/minimind)(纯文本)、
|
| 5 |
+
[miniMind-V](https://github.com/jingyaogong/minimind-v)(视觉多模态)与
|
| 6 |
+
[miniMind-O](https://github.com/jingyaogong/minimind-o)(语音 / 全模态)三套代码。
|
| 7 |
+
|
| 8 |
+
项目采用标准 `src/` 布局(`pip install -e .` 即可安装为 `omni` 包),
|
| 9 |
+
按 **core(组件)/ models(拼装)/ encoders(模态编码器)/ projectors(桥接层)** 分层。
|
| 10 |
+
|
| 11 |
+
## 设计分层
|
| 12 |
+
|
| 13 |
+
- `core/`:可复用模型**底层组件**,按层级细分为独立模块——
|
| 14 |
+
`norm.py`(`RMSNorm`)、`rope.py`(`precompute_freqs_cis` / `apply_rotary_pos_emb` / `repeat_kv`)、
|
| 15 |
+
`attention.py`(`Attention`)、`mlp.py`(`FeedForward` / `MOEFeedForward`)、
|
| 16 |
+
`block.py`(`MiniMindBlock`)、`model.py`(`MiniMindModel` Transformer 主干)。
|
| 17 |
+
- `models/`:把 `core` 组件**拼装**成成品模型——
|
| 18 |
+
`MiniMindForCausalLM`(文本)、`MiniMindVLM`(视觉)、`MiniMindOmni`(语音/全模态)。
|
| 19 |
+
- `encoders/`:外部模态编码器,按模态分目录——`vision/`(SigLIP)、`audio/`(SenseVoice)。
|
| 20 |
+
- `projectors/`:把 encoder 输出**桥接**到 LLM 隐藏维度的拼接层(`MMVisionProjector`、`MMAudioProjector`)。
|
| 21 |
+
- `serve/`:实时语音会话工程层(`SileroVAD`、`RealtimeSession`)。
|
| 22 |
+
|
| 23 |
+
## 目录结构
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
src/omni/
|
| 27 |
+
├── core/ # 模型底层组件(按层级拆分)
|
| 28 |
+
│ ├── norm.py # RMSNorm
|
| 29 |
+
│ ├── rope.py # precompute_freqs_cis / apply_rotary_pos_emb / repeat_kv
|
| 30 |
+
│ ├── attention.py # Attention
|
| 31 |
+
│ ├── mlp.py # FeedForward / MOEFeedForward
|
| 32 |
+
│ ├── block.py # MiniMindBlock
|
| 33 |
+
│ └── model.py # MiniMindModel(Transformer 主干)
|
| 34 |
+
├── models/ # 模型拼装
|
| 35 |
+
│ ├── minimind.py # MiniMindConfig + MiniMindForCausalLM
|
| 36 |
+
│ ├── vlm.py # VLMConfig + MiniMindVLM(视觉多模态)
|
| 37 |
+
│ ├── omni.py # OmniConfig + MiniMindOmni + TalkerModule(语音/全模态)
|
| 38 |
+
│ └── lora.py # LoRA 注入 / 保存 / 合并
|
| 39 |
+
├── encoders/ # 多模态编码器(按模态分目录)
|
| 40 |
+
│ ├── vision/ # SiglipVisionEncoder
|
| 41 |
+
│ └── audio/ # SenseVoiceAudioEncoder
|
| 42 |
+
├── projectors/ # 多模态桥接层
|
| 43 |
+
│ ├── vision.py # MMVisionProjector
|
| 44 |
+
│ └── audio.py # MMAudioProjector
|
| 45 |
+
├── trainers/ # 训练脚本(可直接 python -m 运行)
|
| 46 |
+
│ ├── pretrain.py # 文本预训练
|
| 47 |
+
│ ├── full_sft.py # 文本全量 SFT
|
| 48 |
+
│ ├── lora.py / dpo.py / distillation.py / ppo.py / grpo.py / agent.py
|
| 49 |
+
│ ├── rollout_engine.py # torch / sglang 推理引擎
|
| 50 |
+
│ ├── train_tokenizer.py # tokenizer 训练(学习用)
|
| 51 |
+
│ ├── pretrain_vlm.py # 视觉预训练
|
| 52 |
+
│ ├── full_sft_vlm.py # 视觉 SFT
|
| 53 |
+
│ └── full_sft_omni.py # 全模态 SFT
|
| 54 |
+
├── datasets/ # 数据集(Pretrain/SFT/DPO/RLAIF/Agent/VLM/Omni)
|
| 55 |
+
│ └── lm_dataset.py
|
| 56 |
+
├── utils/ # 工具
|
| 57 |
+
│ ├── training.py # get_lr / init_model / lm_checkpoint / SkipBatchSampler / LMForRewardModel
|
| 58 |
+
│ ├── multimodal.py # init_vlm_model / vlm_checkpoint / init_omni_model / omni_checkpoint
|
| 59 |
+
│ ├── distributed.py # 分布式初始化
|
| 60 |
+
│ └── checkpoint.py # checkpoint 读写辅助
|
| 61 |
+
├── serve/ # 实时语音会话(SileroVAD / RealtimeSession)
|
| 62 |
+
└── __init__.py
|
| 63 |
+
examples/ # 推理 / 服务 / 转换脚本
|
| 64 |
+
├── eval_llm.py # 命令行推理与对话
|
| 65 |
+
├── eval_vlm.py # 视觉多模态推理
|
| 66 |
+
├── eval_omni.py # 全模态推理
|
| 67 |
+
├── serve_openai_api.py # OpenAI 兼容 API 服务
|
| 68 |
+
├── omni_web_demo.py # 网页演示(含实时语音)
|
| 69 |
+
├── eval_toolcall.py # 工具调用评测
|
| 70 |
+
└── convert_model.py # torch <-> transformers 权重互转
|
| 71 |
+
weights/MiniMind2/ # tokenizer 与模型配置(从 MiniMind 迁移)
|
| 72 |
+
configs/ # 训练配置(按需补充)
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## 安装
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
pip install -e .
|
| 79 |
+
# 可选依赖:RL 训练 / API 服务 / 演示
|
| 80 |
+
pip install -e ".[rl,serve,demo]"
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## 快速开始
|
| 84 |
+
|
| 85 |
+
### 推理 / 对话
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
python -m examples.eval_llm --load_from weights/MiniMind2 --weight full_sft
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### 训练
|
| 92 |
+
|
| 93 |
+
每个训练脚本都是 `omni.trainers` 下的一个模块,直接运行即可:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
# 预训练
|
| 97 |
+
python -m omni.trainers.pretrain --data_path dataset/pretrain.jsonl
|
| 98 |
+
# 全量 SFT
|
| 99 |
+
python -m omni.trainers.full_sft --data_path dataset/sft.jsonl
|
| 100 |
+
# LoRA 微调
|
| 101 |
+
python -m omni.trainers.lora --data_path dataset/lora.jsonl
|
| 102 |
+
# DPO / 蒸馏 / PPO / GRPO / Agent RL
|
| 103 |
+
python -m omni.trainers.dpo --data_path dataset/dpo.jsonl
|
| 104 |
+
python -m omni.trainers.distillation --data_path dataset/sft.jsonl
|
| 105 |
+
python -m omni.trainers.grpo --data_path dataset/rlaif.jsonl
|
| 106 |
+
python -m omni.trainers.agent --data_path dataset/agent_rl.jsonl
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
所有脚本参数与原 MiniMind 保持一致(hidden_size / num_hidden_layers / use_moe / data_path 等)。
|
| 110 |
+
|
| 111 |
+
## 与原 MiniMind 的差异
|
| 112 |
+
|
| 113 |
+
- 去除所有 `sys.path` 注入 hack,统一使用 `omni.*` 包导入;
|
| 114 |
+
- `trainer_utils.py` 拆分为 `utils/training.py`(训练工具)、`utils/distributed.py`、`utils/checkpoint.py`;
|
| 115 |
+
- 训练脚本从「`if __name__ == '__main__'` 内联」改为可被 `python -m omni.trainers.<name>` 调用的模块;
|
| 116 |
+
- 预留 `encoders/`(vision/audio)、`projectors/`、`core/` 供多模态扩展。
|
| 117 |
+
|
| 118 |
+
## 多模态扩展方向
|
| 119 |
+
|
| 120 |
+
在 `models/minimind.py` 的 `MiniMindModel` 之上接入:
|
| 121 |
+
|
| 122 |
+
1. `encoders/vision` / `encoders/audio` —— 各自的模态编码器;
|
| 123 |
+
2. `projectors` —— 将编码器输出投影到 LLM 隐藏维度;
|
| 124 |
+
3. 在 `MiniMindModel.forward` 中把投影特征拼接到 `embed_tokens` 之后。
|
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Omni / MiniMind 训练配置示例
|
| 2 |
+
# 用法:将此处参数直接作为命令行参数传给对应训练模块,例如
|
| 3 |
+
# python -m omni.trainers.full_sft --hidden_size 768 --num_hidden_layers 8 --use_moe 0 --data_path dataset/sft.jsonl
|
| 4 |
+
|
| 5 |
+
model:
|
| 6 |
+
hidden_size: 768
|
| 7 |
+
num_hidden_layers: 8
|
| 8 |
+
use_moe: 0 # 1 启用 MoE
|
| 9 |
+
vocab_size: 6400
|
| 10 |
+
max_seq_len: 768
|
| 11 |
+
|
| 12 |
+
train:
|
| 13 |
+
epochs: 2
|
| 14 |
+
batch_size: 16
|
| 15 |
+
learning_rate: 1.0e-5
|
| 16 |
+
accumulation_steps: 1
|
| 17 |
+
grad_clip: 1.0
|
| 18 |
+
dtype: bfloat16
|
| 19 |
+
save_interval: 1000
|
| 20 |
+
log_interval: 100
|
| 21 |
+
from_weight: pretrain # none / pretrain / full_sft / ...
|
| 22 |
+
from_resume: 0
|
| 23 |
+
|
| 24 |
+
paths:
|
| 25 |
+
save_dir: out
|
| 26 |
+
data_path: dataset/sft.jsonl
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MiniMind Datasets
|
| 2 |
+
|
| 3 |
+
将所有下载的数据集文件放置到当前目录.
|
| 4 |
+
|
| 5 |
+
Place the downloaded dataset file in the current directory.
|
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import transformers
|
| 6 |
+
import warnings
|
| 7 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, Qwen3Config, Qwen3ForCausalLM, Qwen3MoeConfig, Qwen3MoeForCausalLM
|
| 8 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 9 |
+
from omni.models.lora import apply_lora, merge_lora
|
| 10 |
+
|
| 11 |
+
warnings.filterwarnings('ignore', category=UserWarning)
|
| 12 |
+
|
| 13 |
+
def convert_torch2transformers_minimind(torch_path, transformers_path, dtype=torch.float16):
|
| 14 |
+
MiniMindConfig.register_for_auto_class()
|
| 15 |
+
MiniMindForCausalLM.register_for_auto_class("AutoModelForCausalLM")
|
| 16 |
+
lm_model = MiniMindForCausalLM(lm_config)
|
| 17 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 18 |
+
state_dict = torch.load(torch_path, map_location=device)
|
| 19 |
+
lm_model.load_state_dict(state_dict, strict=False)
|
| 20 |
+
lm_model = lm_model.to(dtype) # 转换模型权重精度
|
| 21 |
+
model_params = sum(p.numel() for p in lm_model.parameters() if p.requires_grad)
|
| 22 |
+
print(f'模型参数: {model_params / 1e6} 百万 = {model_params / 1e9} B (Billion)')
|
| 23 |
+
lm_model.save_pretrained(transformers_path, safe_serialization=False)
|
| 24 |
+
tokenizer = AutoTokenizer.from_pretrained('../model/')
|
| 25 |
+
tokenizer.save_pretrained(transformers_path)
|
| 26 |
+
# ======= transformers-5.0的兼容低版本写法 =======
|
| 27 |
+
if int(transformers.__version__.split('.')[0]) >= 5:
|
| 28 |
+
tokenizer_config_path, config_path = os.path.join(transformers_path, "tokenizer_config.json"), os.path.join(transformers_path, "config.json")
|
| 29 |
+
json.dump({**json.load(open(tokenizer_config_path, 'r', encoding='utf-8')), "tokenizer_class": "PreTrainedTokenizerFast", "extra_special_tokens": {}}, open(tokenizer_config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
|
| 30 |
+
config = json.load(open(config_path, 'r', encoding='utf-8'))
|
| 31 |
+
config['rope_theta'] = lm_config.rope_theta; config['rope_scaling'] = None; del config['rope_parameters']
|
| 32 |
+
json.dump(config, open(config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
|
| 33 |
+
print(f"模型已保存为 Transformers-MiniMind 格式: {transformers_path}")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# QwenForCausalLM/LlamaForCausalLM结构兼容生态
|
| 37 |
+
def convert_torch2transformers(torch_path, transformers_path, dtype=torch.float16):
|
| 38 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 39 |
+
state_dict = torch.load(torch_path, map_location=device)
|
| 40 |
+
common_config = {
|
| 41 |
+
"vocab_size": lm_config.vocab_size,
|
| 42 |
+
"hidden_size": lm_config.hidden_size,
|
| 43 |
+
"intermediate_size": lm_config.intermediate_size,
|
| 44 |
+
"num_hidden_layers": lm_config.num_hidden_layers,
|
| 45 |
+
"num_attention_heads": lm_config.num_attention_heads,
|
| 46 |
+
"num_key_value_heads": lm_config.num_key_value_heads,
|
| 47 |
+
"head_dim": lm_config.hidden_size // lm_config.num_attention_heads,
|
| 48 |
+
"max_position_embeddings": lm_config.max_position_embeddings,
|
| 49 |
+
"rms_norm_eps": lm_config.rms_norm_eps,
|
| 50 |
+
"rope_theta": lm_config.rope_theta,
|
| 51 |
+
"tie_word_embeddings": lm_config.tie_word_embeddings
|
| 52 |
+
}
|
| 53 |
+
if not lm_config.use_moe:
|
| 54 |
+
qwen_config = Qwen3Config(
|
| 55 |
+
**common_config,
|
| 56 |
+
use_sliding_window=False,
|
| 57 |
+
sliding_window=None
|
| 58 |
+
)
|
| 59 |
+
qwen_model = Qwen3ForCausalLM(qwen_config)
|
| 60 |
+
else:
|
| 61 |
+
qwen_config = Qwen3MoeConfig(
|
| 62 |
+
**common_config,
|
| 63 |
+
num_experts=lm_config.num_experts,
|
| 64 |
+
num_experts_per_tok=lm_config.num_experts_per_tok,
|
| 65 |
+
moe_intermediate_size=lm_config.moe_intermediate_size,
|
| 66 |
+
norm_topk_prob=lm_config.norm_topk_prob
|
| 67 |
+
)
|
| 68 |
+
qwen_model = Qwen3MoeForCausalLM(qwen_config)
|
| 69 |
+
# ======= transformers-5.0的兼容低版本写法 =======
|
| 70 |
+
if int(transformers.__version__.split('.')[0]) >= 5:
|
| 71 |
+
new_sd = {k: v for k, v in state_dict.items() if 'experts.' not in k or 'gate.weight' in k}
|
| 72 |
+
for l in range(lm_config.num_hidden_layers):
|
| 73 |
+
p = f'model.layers.{l}.mlp.experts'
|
| 74 |
+
new_sd[f'{p}.gate_up_proj'] = torch.cat([torch.stack([state_dict[f'{p}.{e}.gate_proj.weight'] for e in range(lm_config.num_experts)]), torch.stack([state_dict[f'{p}.{e}.up_proj.weight'] for e in range(lm_config.num_experts)])], dim=1)
|
| 75 |
+
new_sd[f'{p}.down_proj'] = torch.stack([state_dict[f'{p}.{e}.down_proj.weight'] for e in range(lm_config.num_experts)])
|
| 76 |
+
state_dict = new_sd
|
| 77 |
+
|
| 78 |
+
qwen_model.load_state_dict(state_dict, strict=True)
|
| 79 |
+
qwen_model = qwen_model.to(dtype) # 转换模型权重精度
|
| 80 |
+
qwen_model.save_pretrained(transformers_path)
|
| 81 |
+
model_params = sum(p.numel() for p in qwen_model.parameters() if p.requires_grad)
|
| 82 |
+
print(f'模型参数: {model_params / 1e6} 百万 = {model_params / 1e9} B (Billion)')
|
| 83 |
+
tokenizer = AutoTokenizer.from_pretrained('../model/')
|
| 84 |
+
tokenizer.save_pretrained(transformers_path)
|
| 85 |
+
|
| 86 |
+
# ======= transformers-5.0的兼容低版本写法 =======
|
| 87 |
+
if int(transformers.__version__.split('.')[0]) >= 5:
|
| 88 |
+
tokenizer_config_path, config_path = os.path.join(transformers_path, "tokenizer_config.json"), os.path.join(transformers_path, "config.json")
|
| 89 |
+
json.dump({**json.load(open(tokenizer_config_path, 'r', encoding='utf-8')), "tokenizer_class": "PreTrainedTokenizerFast", "extra_special_tokens": {}}, open(tokenizer_config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
|
| 90 |
+
config = json.load(open(config_path, 'r', encoding='utf-8'))
|
| 91 |
+
config['rope_theta'] = lm_config.rope_theta; config['rope_scaling'] = None; del config['rope_parameters']
|
| 92 |
+
json.dump(config, open(config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
|
| 93 |
+
print(f"模型已保存为 Transformers 格式: {transformers_path}")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def convert_transformers2torch(transformers_path, torch_path):
|
| 97 |
+
model = AutoModelForCausalLM.from_pretrained(transformers_path, trust_remote_code=True)
|
| 98 |
+
torch.save({k: v.cpu().half() for k, v in model.state_dict().items()}, torch_path)
|
| 99 |
+
print(f"模型已保存为 PyTorch 格式: {torch_path}")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def convert_merge_base_lora(base_torch_path, lora_path, merged_torch_path):
|
| 103 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 104 |
+
lm_model = MiniMindForCausalLM(lm_config).to(device)
|
| 105 |
+
state_dict = torch.load(base_torch_path, map_location=device)
|
| 106 |
+
lm_model.load_state_dict(state_dict, strict=False)
|
| 107 |
+
apply_lora(lm_model)
|
| 108 |
+
merge_lora(lm_model, lora_path, merged_torch_path)
|
| 109 |
+
print(f"LoRA 已合并并保存为基模结构 PyTorch 格式: {merged_torch_path}")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def convert_jinja_to_json(jinja_path):
|
| 113 |
+
with open(jinja_path, 'r') as f: template = f.read()
|
| 114 |
+
escaped = json.dumps(template)
|
| 115 |
+
print(f'"chat_template": {escaped}')
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def convert_json_to_jinja(json_file_path, output_path):
|
| 119 |
+
with open(json_file_path, 'r') as f: config = json.load(f)
|
| 120 |
+
template = config['chat_template']
|
| 121 |
+
with open(output_path, 'w') as f: f.write(template)
|
| 122 |
+
print(f"模板已保存为 jinja 文件: {output_path}")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
if __name__ == '__main__':
|
| 126 |
+
lm_config = MiniMindConfig(hidden_size=768, num_hidden_layers=8, max_seq_len=8192, use_moe=False)
|
| 127 |
+
|
| 128 |
+
# convert torch to transformers
|
| 129 |
+
torch_path = f"../out/full_sft_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
|
| 130 |
+
transformers_path = '../minimind-3'
|
| 131 |
+
convert_torch2transformers(torch_path, transformers_path)
|
| 132 |
+
|
| 133 |
+
# # merge lora
|
| 134 |
+
# base_torch_path = f"../out/full_sft_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
|
| 135 |
+
# lora_path = f"../out/lora_identity_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
|
| 136 |
+
# merged_torch_path = f"../out/merge_identity_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
|
| 137 |
+
# convert_merge_base_lora(base_torch_path, lora_path, merged_torch_path)
|
| 138 |
+
|
| 139 |
+
# convert_transformers2torch(transformers_path, torch_path)
|
| 140 |
+
# convert_json_to_jinja('../model/tokenizer_config.json', '../model/chat_template.jinja')
|
| 141 |
+
# convert_jinja_to_json('../model/chat_template.jinja')
|
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import argparse
|
| 3 |
+
import random
|
| 4 |
+
import warnings
|
| 5 |
+
import torch
|
| 6 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
|
| 7 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 8 |
+
from omni.models.lora import * # noqa: F401,F403
|
| 9 |
+
from omni.utils.training import setup_seed, get_model_params
|
| 10 |
+
warnings.filterwarnings('ignore')
|
| 11 |
+
|
| 12 |
+
def init_model(args):
|
| 13 |
+
tokenizer = AutoTokenizer.from_pretrained(args.load_from)
|
| 14 |
+
if 'model' in args.load_from:
|
| 15 |
+
model = MiniMindForCausalLM(MiniMindConfig(
|
| 16 |
+
hidden_size=args.hidden_size,
|
| 17 |
+
num_hidden_layers=args.num_hidden_layers,
|
| 18 |
+
use_moe=bool(args.use_moe),
|
| 19 |
+
inference_rope_scaling=args.inference_rope_scaling
|
| 20 |
+
))
|
| 21 |
+
moe_suffix = '_moe' if args.use_moe else ''
|
| 22 |
+
ckp = f'./{args.save_dir}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
|
| 23 |
+
model.load_state_dict(torch.load(ckp, map_location=args.device), strict=True)
|
| 24 |
+
if args.lora_weight != 'None':
|
| 25 |
+
apply_lora(model)
|
| 26 |
+
load_lora(model, f'./{args.save_dir}/{args.lora_weight}_{args.hidden_size}.pth')
|
| 27 |
+
else:
|
| 28 |
+
model = AutoModelForCausalLM.from_pretrained(args.load_from, trust_remote_code=True)
|
| 29 |
+
get_model_params(model, model.config)
|
| 30 |
+
return model.half().eval().to(args.device), tokenizer
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
parser = argparse.ArgumentParser(description="MiniMind模型推理与对话")
|
| 34 |
+
parser.add_argument('--load_from', default='model', type=str, help="模型加载路径(model=原生torch权重,其他路径=transformers格式)")
|
| 35 |
+
parser.add_argument('--save_dir', default='out', type=str, help="模型权重目录")
|
| 36 |
+
parser.add_argument('--weight', default='full_sft', type=str, help="权重名称前缀(pretrain, full_sft, rlhf, reason, ppo_actor, grpo, spo)")
|
| 37 |
+
parser.add_argument('--lora_weight', default='None', type=str, help="LoRA权重名称(None表示不使用,可选:lora_identity, lora_medical)")
|
| 38 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 39 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 40 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 41 |
+
parser.add_argument('--inference_rope_scaling', default=False, action='store_true', help="启用RoPE位置编码外推(4倍,仅解决位置编码问题)")
|
| 42 |
+
parser.add_argument('--max_new_tokens', default=8192, type=int, help="最大生成长度(注意:并非模型实际长文本能力)")
|
| 43 |
+
parser.add_argument('--temperature', default=0.85, type=float, help="生成温度,控制随机性(0-1,越大越随机)")
|
| 44 |
+
parser.add_argument('--top_p', default=0.95, type=float, help="nucleus采样阈值(0-1)")
|
| 45 |
+
parser.add_argument('--open_thinking', default=0, type=int, help="是否开启自适应思考(0=否,1=是)")
|
| 46 |
+
parser.add_argument('--historys', default=0, type=int, help="携带历史对话轮数(需为偶数,0表示不携带历史)")
|
| 47 |
+
parser.add_argument('--show_speed', default=1, type=int, help="显示decode速度(tokens/s)")
|
| 48 |
+
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', type=str, help="运行设备")
|
| 49 |
+
args = parser.parse_args()
|
| 50 |
+
|
| 51 |
+
prompts = [
|
| 52 |
+
'你有什么特长?',
|
| 53 |
+
'为什么天空是蓝色的',
|
| 54 |
+
'请用Python写一个计算斐波那契数列的函数',
|
| 55 |
+
'解释一下"光合作用"的基本过程',
|
| 56 |
+
'如果明天下雨,我应该如何出门',
|
| 57 |
+
'比较一下猫和狗作为宠物的优缺点',
|
| 58 |
+
'解释什么是机器学习',
|
| 59 |
+
'推荐一些中国的美食'
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
conversation = []
|
| 63 |
+
model, tokenizer = init_model(args)
|
| 64 |
+
input_mode = int(input('[0] 自动测试\n[1] 手动输入\n'))
|
| 65 |
+
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 66 |
+
|
| 67 |
+
prompt_iter = prompts if input_mode == 0 else iter(lambda: input('💬: '), '')
|
| 68 |
+
for prompt in prompt_iter:
|
| 69 |
+
setup_seed(random.randint(0, 31415926))
|
| 70 |
+
if input_mode == 0: print(f'💬: {prompt}')
|
| 71 |
+
conversation = conversation[-args.historys:] if args.historys else []
|
| 72 |
+
conversation.append({"role": "user", "content": prompt})
|
| 73 |
+
if 'pretrain' in args.weight:
|
| 74 |
+
inputs = tokenizer.bos_token + prompt
|
| 75 |
+
else:
|
| 76 |
+
inputs = tokenizer.apply_chat_template(conversation, tokenize=False, add_generation_prompt=True, open_thinking=bool(args.open_thinking))
|
| 77 |
+
|
| 78 |
+
inputs = tokenizer(inputs, return_tensors="pt", truncation=True).to(args.device)
|
| 79 |
+
|
| 80 |
+
print('🧠: ', end='')
|
| 81 |
+
st = time.time()
|
| 82 |
+
generated_ids = model.generate(
|
| 83 |
+
inputs=inputs["input_ids"], attention_mask=inputs["attention_mask"],
|
| 84 |
+
max_new_tokens=args.max_new_tokens, do_sample=True, streamer=streamer,
|
| 85 |
+
pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id,
|
| 86 |
+
top_p=args.top_p, temperature=args.temperature, repetition_penalty=1
|
| 87 |
+
)
|
| 88 |
+
response = tokenizer.decode(generated_ids[0][len(inputs["input_ids"][0]):], skip_special_tokens=True)
|
| 89 |
+
conversation.append({"role": "assistant", "content": response})
|
| 90 |
+
gen_tokens = len(generated_ids[0]) - len(inputs["input_ids"][0])
|
| 91 |
+
print(f'\n[Speed]: {gen_tokens / (time.time() - st):.2f} tokens/s\n\n') if args.show_speed else print('\n\n')
|
| 92 |
+
|
| 93 |
+
if __name__ == "__main__":
|
| 94 |
+
main()
|
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import random
|
| 4 |
+
import time
|
| 5 |
+
import warnings
|
| 6 |
+
import torch
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 9 |
+
from omni.models import MiniMindOmni, OmniConfig
|
| 10 |
+
from omni.datasets.lm_dataset import OmniDataset
|
| 11 |
+
from omni.utils import setup_seed, log_model_params
|
| 12 |
+
warnings.filterwarnings('ignore')
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def init_model(args):
|
| 16 |
+
tokenizer = AutoTokenizer.from_pretrained(args.load_from)
|
| 17 |
+
if 'model' in args.load_from:
|
| 18 |
+
moe_suffix = '_moe' if args.use_moe else ''
|
| 19 |
+
ckp = f'./{args.save_dir}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
|
| 20 |
+
model = MiniMindOmni(
|
| 21 |
+
OmniConfig(
|
| 22 |
+
hidden_size=args.hidden_size,
|
| 23 |
+
num_hidden_layers=args.num_hidden_layers,
|
| 24 |
+
use_moe=bool(args.use_moe)
|
| 25 |
+
),
|
| 26 |
+
audio_encoder_path="./model/SenseVoiceSmall",
|
| 27 |
+
vision_model_path="./model/siglip2-base-p32-256-ve"
|
| 28 |
+
)
|
| 29 |
+
model.load_state_dict(torch.load(ckp, map_location=args.device), strict=False)
|
| 30 |
+
else:
|
| 31 |
+
model = AutoModelForCausalLM.from_pretrained(args.load_from, trust_remote_code=True)
|
| 32 |
+
model.audio_encoder, model.audio_processor = MiniMindOmni.load_sensevoice("./model/SenseVoiceSmall")
|
| 33 |
+
model.vision_encoder, model.vision_processor = MiniMindOmni.load_vision("./model/siglip2-base-p32-256-ve")
|
| 34 |
+
log_model_params(model)
|
| 35 |
+
if model.audio_encoder is not None: model.audio_encoder.to(args.device)
|
| 36 |
+
if model.vision_encoder is not None: model.vision_encoder.to(args.device)
|
| 37 |
+
from transformers import MimiModel
|
| 38 |
+
model.mimi_model = MimiModel.from_pretrained("./model/mimi").eval()
|
| 39 |
+
return model.half().eval().to(args.device), tokenizer
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def eval_sample(model, tokenizer, args, idx, prompt, audio_inputs, output_name, pixel_values=None, history=None, audio_lens=None, ref_codes=None, spk_emb=None):
|
| 43 |
+
import soundfile as sf
|
| 44 |
+
from pydub import AudioSegment
|
| 45 |
+
messages = (history or []) + [{"role": "user", "content": prompt}]
|
| 46 |
+
inputs_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, open_thinking=bool(args.open_thinking))
|
| 47 |
+
x = torch.tensor(tokenizer(inputs_text).data['input_ids'], dtype=torch.long, device=args.device)[None, ...]
|
| 48 |
+
|
| 49 |
+
audio_frames = []
|
| 50 |
+
with torch.no_grad():
|
| 51 |
+
res_y = model.generate(x, tokenizer.eos_token_id, max_new_tokens=args.max_new_tokens,
|
| 52 |
+
temperature=args.temperature, top_p=args.top_p, stream=True,
|
| 53 |
+
return_audio_codes=True, open_thinking=bool(args.open_thinking),
|
| 54 |
+
audio_inputs=audio_inputs, audio_lens=audio_lens, pixel_values=pixel_values,
|
| 55 |
+
ref_codes=ref_codes, spk_emb=spk_emb)
|
| 56 |
+
print('📒 [Thinker]: ', end='', flush=True)
|
| 57 |
+
history_idx = 0
|
| 58 |
+
for y, audio_frame in res_y:
|
| 59 |
+
if y is not None:
|
| 60 |
+
answer = tokenizer.decode(y[0].tolist(), skip_special_tokens=True)
|
| 61 |
+
if answer and answer[-1] != '�':
|
| 62 |
+
print(answer[history_idx:], end='', flush=True)
|
| 63 |
+
history_idx = len(answer)
|
| 64 |
+
if audio_frame:
|
| 65 |
+
audio_frames.append(audio_frame)
|
| 66 |
+
print()
|
| 67 |
+
|
| 68 |
+
if audio_frames:
|
| 69 |
+
print(f'🎹 [Talker]: {len(audio_frames)} frames', end=" ")
|
| 70 |
+
if args.decode_audio:
|
| 71 |
+
try:
|
| 72 |
+
codes = [f for f in audio_frames if f and len(f) == 8]
|
| 73 |
+
if not codes:
|
| 74 |
+
print('⚠️ 生成的Mimi codes为空,跳过保存。')
|
| 75 |
+
return
|
| 76 |
+
mimi_codes = torch.tensor(codes, dtype=torch.long).T.unsqueeze(0).to(args.device)
|
| 77 |
+
filtered = torch.where(mimi_codes >= 2049, torch.zeros_like(mimi_codes), mimi_codes)
|
| 78 |
+
audio = model.mimi_model.decode(filtered).audio_values
|
| 79 |
+
output_path = os.path.join(args.output_dir, output_name)
|
| 80 |
+
wav_path = output_path.rsplit('.', 1)[0] + '.wav'
|
| 81 |
+
sf.write(wav_path, audio.squeeze().float().cpu().numpy(), 24000)
|
| 82 |
+
AudioSegment.from_wav(wav_path).export(output_path, format='mp3', bitrate='64k')
|
| 83 |
+
os.remove(wav_path)
|
| 84 |
+
print(f'| Audio decoded to: {output_path}')
|
| 85 |
+
except Exception as e:
|
| 86 |
+
print(f'⚠️ 保存音频失败: {str(e)}')
|
| 87 |
+
else:
|
| 88 |
+
print("(decode_audio=off)\n")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def main():
|
| 92 |
+
parser = argparse.ArgumentParser(description="MiniMind-O Chat")
|
| 93 |
+
parser.add_argument('--load_from', default='model', type=str, help="模型加载路径(model=原生torch权重)")
|
| 94 |
+
parser.add_argument('--save_dir', default='out', type=str, help="模型权重目录")
|
| 95 |
+
parser.add_argument('--weight', default='sft_omni', type=str, help="权重名称前缀")
|
| 96 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 97 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 98 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构")
|
| 99 |
+
parser.add_argument('--max_new_tokens', default=512, type=int, help="最大生成长度")
|
| 100 |
+
parser.add_argument('--temperature', default=0.7, type=float, help="Thinker生成温度")
|
| 101 |
+
parser.add_argument('--top_p', default=0.85, type=float, help="nucleus采样阈值")
|
| 102 |
+
parser.add_argument('--output_dir', default='./output_audio/', type=str, help="输出音频保存目录")
|
| 103 |
+
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', type=str, help="运行设备")
|
| 104 |
+
parser.add_argument('--audio_dir', default='./dataset/eval_omni/', type=str, help="测试音频目录")
|
| 105 |
+
parser.add_argument('--image_dir', default='./dataset/eval_omni/', type=str, help="测试图像目录")
|
| 106 |
+
parser.add_argument('--open_thinking', default=0, type=int, help="是否开启思考模式(0=否,1=是)(思考模式下禁用audio输出)")
|
| 107 |
+
parser.add_argument('--decode_audio', default=1, type=int, help="是否解码音频输出(0=否,1=是)")
|
| 108 |
+
parser.add_argument('--mode', default='0', type=str, help="评估模式:-1=all 0=text 1=multi 2=audio 3=clone 4=image 5=mix(逗号组合,如 2,5)")
|
| 109 |
+
parser.add_argument('--prompt_lang', default=0, type=int, choices=[0, 1, 2], help="问题语言:0=英文 1=中文 2=英文+中文")
|
| 110 |
+
args = parser.parse_args()
|
| 111 |
+
modes = set(args.mode.replace(',', '').replace('-1', '012345'))
|
| 112 |
+
|
| 113 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 114 |
+
model, tokenizer = init_model(args)
|
| 115 |
+
setup_seed(int(time.time()) % 31415926)
|
| 116 |
+
|
| 117 |
+
if '0' in modes:
|
| 118 |
+
print('\n\n==================== text -> {text, audio} ====================')
|
| 119 |
+
test_prompts_en = [
|
| 120 |
+
"Tell me an interesting fact about space.", "How do I make a cup of coffee?", "What's the weather like today?",
|
| 121 |
+
"Will it rain tomorrow?", "Tell me a joke.", "Can you sing a song for me?", "Please introduce yourself."
|
| 122 |
+
]
|
| 123 |
+
test_prompts_zh = [
|
| 124 |
+
"告诉我一个关于太空的有趣事实。", "如何制作一杯咖啡?", "今天的天气怎么样?",
|
| 125 |
+
"明天会下雨吗?", "给我讲个笑话吧", "你能为我唱首歌吗?", "介绍一下你自己"
|
| 126 |
+
]
|
| 127 |
+
test_prompts = [test_prompts_en, test_prompts_zh, test_prompts_en + test_prompts_zh][args.prompt_lang]
|
| 128 |
+
for idx, prompt in enumerate(test_prompts):
|
| 129 |
+
print(f'\n📝 [text-{idx+1}]: {prompt}')
|
| 130 |
+
eval_sample(model, tokenizer, args, idx, prompt, None, f"text-{idx:02d}.mp3")
|
| 131 |
+
|
| 132 |
+
if '1' in modes:
|
| 133 |
+
print('\n\n==================== multi-turn -> {text, audio} ====================')
|
| 134 |
+
multi_turn_tests_zh = [
|
| 135 |
+
{
|
| 136 |
+
"history": [
|
| 137 |
+
{"role": "user", "content": "你好"},
|
| 138 |
+
{"role": "assistant", "content": "你好!有什么可以帮你的吗?"}
|
| 139 |
+
],
|
| 140 |
+
"prompt": "我想找点事做,你有什么建议吗?"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"history": [
|
| 144 |
+
{"role": "user", "content": "你好"},
|
| 145 |
+
{"role": "assistant", "content": "你好!有什么可以帮你的吗?"},
|
| 146 |
+
{"role": "user", "content": "我想找点事做,你有什么建议吗?"},
|
| 147 |
+
{"role": "assistant", "content": "可以听听音乐或者看看书,放松一下心情。"}
|
| 148 |
+
],
|
| 149 |
+
"prompt": "好的,那我去照做了,谢谢你"
|
| 150 |
+
}
|
| 151 |
+
]
|
| 152 |
+
multi_turn_tests_en = [
|
| 153 |
+
{
|
| 154 |
+
"history": [
|
| 155 |
+
{"role": "user", "content": "Hello"},
|
| 156 |
+
{"role": "assistant", "content": "Hello! How can I help you?"}
|
| 157 |
+
],
|
| 158 |
+
"prompt": "I want to find something to do. Do you have any suggestions?"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"history": [
|
| 162 |
+
{"role": "user", "content": "Hello"},
|
| 163 |
+
{"role": "assistant", "content": "Hello! How can I help you?"},
|
| 164 |
+
{"role": "user", "content": "I want to find something to do. Do you have any suggestions?"},
|
| 165 |
+
{"role": "assistant", "content": "You can listen to music or read a book to relax a little."}
|
| 166 |
+
],
|
| 167 |
+
"prompt": "Okay, I will try that. Thank you."
|
| 168 |
+
}
|
| 169 |
+
]
|
| 170 |
+
multi_turn_tests = [multi_turn_tests_en, multi_turn_tests_zh, multi_turn_tests_en + multi_turn_tests_zh][args.prompt_lang]
|
| 171 |
+
for idx, test in enumerate(multi_turn_tests):
|
| 172 |
+
print(f'\n💬 [multi-{idx+1}]')
|
| 173 |
+
for msg in test["history"]: print(f' {msg["role"]}: {msg["content"]}')
|
| 174 |
+
print(f' user: {test["prompt"]}')
|
| 175 |
+
eval_sample(model, tokenizer, args, idx, test["prompt"], None, f"multi-{idx:02d}.mp3", history=test["history"])
|
| 176 |
+
|
| 177 |
+
if '2' in modes:
|
| 178 |
+
print('\n\n==================== audio -> {text, audio} ====================')
|
| 179 |
+
audio_files_en = sorted([f for f in os.listdir(args.audio_dir) if f.startswith('audio-en-') and f.lower().endswith(('.mp3', '.wav'))])
|
| 180 |
+
audio_files_zh = sorted([f for f in os.listdir(args.audio_dir) if f.startswith('audio-zh-') and f.lower().endswith(('.mp3', '.wav'))])
|
| 181 |
+
audio_files = [audio_files_en, audio_files_zh, audio_files_en + audio_files_zh][args.prompt_lang]
|
| 182 |
+
for idx, audio_file in enumerate(audio_files):
|
| 183 |
+
print(f'\n🎤 [audio-{idx+1}]: {audio_file}')
|
| 184 |
+
mel, valid_len = OmniDataset.process_audio(os.path.join(args.audio_dir, audio_file), model.audio_processor)
|
| 185 |
+
audio_inputs = mel.unsqueeze(0).to(args.device)
|
| 186 |
+
audio_lens = torch.tensor([valid_len], device=args.device)
|
| 187 |
+
audio_token_len = valid_len or 1
|
| 188 |
+
prompt = model.config.audio_special_token * audio_token_len
|
| 189 |
+
eval_sample(model, tokenizer, args, idx, prompt, audio_inputs, f"audio-{idx:02d}-{os.path.splitext(audio_file)[0]}.mp3", audio_lens=audio_lens)
|
| 190 |
+
|
| 191 |
+
if '3' in modes:
|
| 192 |
+
print('\n\n==================== clone voice -> {text, audio} ====================')
|
| 193 |
+
clone_prompts_en = ["Hello, please introduce yourself.", "What's the weather like today?", "Tell me a joke."]
|
| 194 |
+
clone_prompts_zh = ["你好,请介绍一下你自己。", "今天天气怎么样?", "给我讲个笑话吧"]
|
| 195 |
+
clone_prompts = [clone_prompts_en, clone_prompts_zh, clone_prompts_en + clone_prompts_zh][args.prompt_lang]
|
| 196 |
+
voices_pt = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'model', 'speaker', 'voices_unseen.pt')
|
| 197 |
+
voices = [('default', None, None)]
|
| 198 |
+
if os.path.exists(voices_pt):
|
| 199 |
+
voice_data = torch.load(voices_pt, map_location=args.device)
|
| 200 |
+
for speaker, v in sorted(voice_data.items()):
|
| 201 |
+
rc = v['ref_codes'].unsqueeze(0).to(args.device)
|
| 202 |
+
se = v['spk_emb'].half().unsqueeze(0).to(args.device) if 'spk_emb' in v else None
|
| 203 |
+
voices.append((speaker, rc, se))
|
| 204 |
+
for speaker, rc, se in voices:
|
| 205 |
+
info = f'ref_codes: {rc.shape[2]} frames, spk_emb: {"+" if se is not None else "-"}' if rc is not None else ('spk_emb only' if se is not None else 'default')
|
| 206 |
+
print(f'\n🎵 [clone: {speaker}] {info}')
|
| 207 |
+
for idx, prompt in enumerate(clone_prompts):
|
| 208 |
+
print(f' 📝 [text-{idx+1}]: {prompt}')
|
| 209 |
+
history = [{"role": "system", "content": "你是一个专业的语音助手,请用给定的音色风格来回答用户的问题。请尽量详细地回答,给出有价值的信息。"}]
|
| 210 |
+
eval_sample(model, tokenizer, args, idx, prompt, None, f"clone-{speaker}-{idx:02d}.mp3", ref_codes=rc, history=history, spk_emb=se)
|
| 211 |
+
|
| 212 |
+
if '4' in modes:
|
| 213 |
+
print('\n\n==================== image -> {text, audio} ====================')
|
| 214 |
+
image_files = sorted([f for f in os.listdir(args.image_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))])
|
| 215 |
+
for idx, image_file in enumerate(image_files):
|
| 216 |
+
print(f'\n🖼️ [image-{idx+1}]: {image_file}')
|
| 217 |
+
image = Image.open(os.path.join(args.image_dir, image_file)).convert('RGB')
|
| 218 |
+
pixel_values = {k: v.to(args.device) for k, v in model.vision_processor(images=image, return_tensors="pt").items()}
|
| 219 |
+
prompts = [["Please describe this image."], ["请描述这张图片"], ["Please describe this image.", "请描述这张图片"]][args.prompt_lang]
|
| 220 |
+
for lang_idx, prompt_text in enumerate(prompts):
|
| 221 |
+
prompt = prompt_text + "\n\n" + model.config.image_special_token * model.config.image_token_len
|
| 222 |
+
eval_sample(model, tokenizer, args, idx, prompt, None, f"image-{idx:02d}-{lang_idx}-{os.path.splitext(image_file)[0]}.mp3", pixel_values=pixel_values)
|
| 223 |
+
|
| 224 |
+
if '5' in modes:
|
| 225 |
+
print('\n\n==================== text+audio+image -> {text, audio} ====================')
|
| 226 |
+
img_audio_files = sorted([f for f in os.listdir(args.audio_dir) if f.startswith('img-') and f.lower().endswith(('.mp3', '.wav'))])
|
| 227 |
+
image_files = sorted([f for f in os.listdir(args.image_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))])
|
| 228 |
+
text_hints = [["Please answer me: "], ["请回答我:"], ["Please answer me: ", "请回答我:"]][args.prompt_lang]
|
| 229 |
+
for idx, image_file in enumerate(image_files):
|
| 230 |
+
audio_file = random.choice(img_audio_files)
|
| 231 |
+
image = Image.open(os.path.join(args.image_dir, image_file)).convert('RGB')
|
| 232 |
+
pixel_values = {k: v.to(args.device) for k, v in model.vision_processor(images=image, return_tensors="pt").items()}
|
| 233 |
+
for lang_idx, text_hint in enumerate(text_hints):
|
| 234 |
+
print(f'\n🌀 [mix-{idx+1}-{lang_idx}]: {text_hint} | {audio_file} | {image_file}')
|
| 235 |
+
mel, valid_len = OmniDataset.process_audio(os.path.join(args.audio_dir, audio_file), model.audio_processor)
|
| 236 |
+
audio_inputs = mel.unsqueeze(0).to(args.device)
|
| 237 |
+
audio_lens = torch.tensor([valid_len], device=args.device)
|
| 238 |
+
audio_token_len = valid_len or 1
|
| 239 |
+
prompt = text_hint + model.config.audio_special_token * audio_token_len + "\n\n" + model.config.image_special_token * model.config.image_token_len
|
| 240 |
+
eval_sample(model, tokenizer, args, idx, prompt, audio_inputs, f"mix-{idx:02d}-{lang_idx}-{os.path.splitext(image_file)[0]}.mp3", pixel_values=pixel_values, audio_lens=audio_lens)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
if __name__ == "__main__":
|
| 244 |
+
main()
|
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
import random
|
| 6 |
+
import argparse
|
| 7 |
+
import warnings
|
| 8 |
+
import torch
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
|
| 11 |
+
from openai import OpenAI
|
| 12 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 13 |
+
from omni.utils.training import setup_seed, get_model_params
|
| 14 |
+
warnings.filterwarnings('ignore')
|
| 15 |
+
|
| 16 |
+
TOOLS = [
|
| 17 |
+
{"type": "function", "function": {"name": "calculate_math", "description": "计算数学表达式的结果,支持加减乘除、幂运算、开方等", "parameters": {"type": "object", "properties": {"expression": {"type": "string", "description": "数学表达式,如123+456、2**10、sqrt(144)"}}, "required": ["expression"]}}},
|
| 18 |
+
{"type": "function", "function": {"name": "get_current_time", "description": "获取当前日期和时间,支持指定时区", "parameters": {"type": "object", "properties": {"timezone": {"type": "string", "description": "时区名称,如Asia/Shanghai、America/New_York", "default": "Asia/Shanghai"}}, "required": []}}},
|
| 19 |
+
{"type": "function", "function": {"name": "random_number", "description": "生成指定范围内的随机数", "parameters": {"type": "object", "properties": {"min": {"type": "integer", "description": "最小值", "default": 0}, "max": {"type": "integer", "description": "最大值", "default": 100}}, "required": []}}},
|
| 20 |
+
{"type": "function", "function": {"name": "text_length", "description": "计算文本的字符数和单词数", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要统计的文本"}}, "required": ["text"]}}},
|
| 21 |
+
{"type": "function", "function": {"name": "unit_converter", "description": "进行单位换算,支持长度、重量、温度等", "parameters": {"type": "object", "properties": {"value": {"type": "number", "description": "要转换的数值"}, "from_unit": {"type": "string", "description": "源单位,如km、miles、kg、pounds、celsius、fahrenheit"}, "to_unit": {"type": "string", "description": "目标单位"}}, "required": ["value", "from_unit", "to_unit"]}}},
|
| 22 |
+
{"type": "function", "function": {"name": "get_current_weather", "description": "获取指定城市的当前天气信息,包括温度、湿度和天气状况", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "城市名称,如北京、上海、New York"}, "unit": {"type": "string", "description": "温度单位,celsius或fahrenheit", "enum": ["celsius", "fahrenheit"], "default": "celsius"}}, "required": ["location"]}}},
|
| 23 |
+
{"type": "function", "function": {"name": "get_exchange_rate", "description": "查询两种货币之间的实时汇率", "parameters": {"type": "object", "properties": {"from_currency": {"type": "string", "description": "源货币代码,如USD、CNY、EUR"}, "to_currency": {"type": "string", "description": "目标货币代码,如USD、CNY、EUR"}}, "required": ["from_currency", "to_currency"]}}},
|
| 24 |
+
{"type": "function", "function": {"name": "translate_text", "description": "将文本翻译成目标语言", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要翻译的文本"}, "target_language": {"type": "string", "description": "目标语言,如english、chinese、japanese、french"}}, "required": ["text", "target_language"]}}},
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
MOCK_RESULTS = {
|
| 28 |
+
"calculate_math": lambda args: {"result": str(eval(str(args.get("expression", "0")).replace("^", "**").replace("×", "*").replace("÷", "/").replace("−", "-").replace("²", "**2").replace("³", "**3").replace("(", "(").replace(")", ")")))},
|
| 29 |
+
"get_current_time": lambda args: {"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timezone": args.get("timezone", "Asia/Shanghai")},
|
| 30 |
+
"random_number": lambda args: {"result": random.randint(int(args.get("min", 0)), int(args.get("max", 100)))},
|
| 31 |
+
"text_length": lambda args: {"characters": len(args.get("text", "")), "words": len(args.get("text", "").split())},
|
| 32 |
+
"unit_converter": lambda args: {"result": round(float(args.get("value", 0)) * 0.621371, 2), "from": f"{args.get('value', 0)} {args.get('from_unit', '')}", "to": args.get("to_unit", "")},
|
| 33 |
+
"get_current_weather": lambda args: {"city": args.get("location"), "temperature": "22°C", "humidity": "65%", "condition": "晴"},
|
| 34 |
+
"get_exchange_rate": lambda args: {"from": args.get("from_currency", ""), "to": args.get("to_currency", ""), "rate": 7.15},
|
| 35 |
+
"translate_text": lambda args: {"translated": "hello world"},
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
TOOL_MAP = {t["function"]["name"]: t for t in TOOLS}
|
| 39 |
+
|
| 40 |
+
def get_tools(names):
|
| 41 |
+
return [TOOL_MAP[n] for n in names]
|
| 42 |
+
|
| 43 |
+
TEST_CASES = [
|
| 44 |
+
{"prompt": "帮我算一下 256 乘以 37 等于多少", "tools": ["calculate_math", "get_current_time"]},
|
| 45 |
+
{"prompt": "现在几点了?", "tools": ["get_current_time", "random_number"]},
|
| 46 |
+
{"prompt": "帮我把100公里换算成英里", "tools": ["unit_converter", "calculate_math"]},
|
| 47 |
+
{"prompt": "帮我生成一个1到1000的随机数,然后计算它的平方", "tools": ["random_number", "calculate_math", "text_length"]},
|
| 48 |
+
{"prompt": "北京今天天气怎么样?", "tools": ["get_current_weather", "get_current_time"]},
|
| 49 |
+
{"prompt": "查一下美元兑人民币汇率", "tools": ["get_exchange_rate", "get_current_time"]},
|
| 50 |
+
{"prompt": "把'你好世界'翻译成英文", "tools": ["translate_text", "text_length"]},
|
| 51 |
+
{"prompt": "What is the weather in Tokyo? Also convert 30 celsius to fahrenheit.", "tools": ["get_current_weather", "unit_converter", "get_current_time"]},
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def init_model(args):
|
| 56 |
+
tokenizer = AutoTokenizer.from_pretrained(args.load_from)
|
| 57 |
+
if 'model' in args.load_from:
|
| 58 |
+
model = MiniMindForCausalLM(MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe)))
|
| 59 |
+
moe_suffix = '_moe' if args.use_moe else ''
|
| 60 |
+
ckp = f'./{args.save_dir}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
|
| 61 |
+
model.load_state_dict(torch.load(ckp, map_location=args.device), strict=True)
|
| 62 |
+
else:
|
| 63 |
+
model = AutoModelForCausalLM.from_pretrained(args.load_from, trust_remote_code=True)
|
| 64 |
+
get_model_params(model, model.config)
|
| 65 |
+
return model.half().eval().to(args.device), tokenizer
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def parse_tool_calls(text):
|
| 69 |
+
matches = re.findall(r'<tool_call>(.*?)</tool_call>', text, re.DOTALL)
|
| 70 |
+
calls = []
|
| 71 |
+
for m in matches:
|
| 72 |
+
try:
|
| 73 |
+
calls.append(json.loads(m.strip()))
|
| 74 |
+
except Exception:
|
| 75 |
+
pass
|
| 76 |
+
return calls
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def parse_tool_call_from_text(content):
|
| 80 |
+
pattern = r'<tool_call>\s*(\{.*?\})\s*</tool_call>'
|
| 81 |
+
matches = re.findall(pattern, content, re.DOTALL)
|
| 82 |
+
if not matches:
|
| 83 |
+
return None
|
| 84 |
+
tool_calls = []
|
| 85 |
+
for i, match in enumerate(matches):
|
| 86 |
+
try:
|
| 87 |
+
data = json.loads(match)
|
| 88 |
+
tool_calls.append({
|
| 89 |
+
"id": f"call_{i}",
|
| 90 |
+
"function": {"name": data.get("name", ""), "arguments": json.dumps(data.get("arguments", {}), ensure_ascii=False)}
|
| 91 |
+
})
|
| 92 |
+
except Exception:
|
| 93 |
+
pass
|
| 94 |
+
return tool_calls if tool_calls else None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def execute_tool(call, arguments=None):
|
| 98 |
+
name = call.get("name", "") if isinstance(call, dict) else call
|
| 99 |
+
try:
|
| 100 |
+
raw_args = call.get("arguments", {}) if isinstance(call, dict) else arguments
|
| 101 |
+
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
| 102 |
+
except Exception:
|
| 103 |
+
args = {}
|
| 104 |
+
fn = MOCK_RESULTS.get(name)
|
| 105 |
+
if not fn:
|
| 106 |
+
return {"error": f"未知工具: {name}"}
|
| 107 |
+
try:
|
| 108 |
+
return fn(args)
|
| 109 |
+
except Exception as e:
|
| 110 |
+
return {"error": f"工具执行失败: {str(e)[:80]}"}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def generate(model, tokenizer, messages, tools, args):
|
| 114 |
+
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 115 |
+
input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools, open_thinking=False)
|
| 116 |
+
inputs = tokenizer(input_text, return_tensors="pt", truncation=True).to(args.device)
|
| 117 |
+
st = time.time()
|
| 118 |
+
print('🧠: ', end='')
|
| 119 |
+
generated_ids = model.generate(
|
| 120 |
+
inputs["input_ids"], attention_mask=inputs["attention_mask"],
|
| 121 |
+
max_new_tokens=args.max_new_tokens, do_sample=True, streamer=streamer,
|
| 122 |
+
pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id,
|
| 123 |
+
top_p=args.top_p, temperature=args.temperature
|
| 124 |
+
)
|
| 125 |
+
response = tokenizer.decode(generated_ids[0][len(inputs["input_ids"][0]):], skip_special_tokens=True)
|
| 126 |
+
gen_tokens = len(generated_ids[0]) - len(inputs["input_ids"][0])
|
| 127 |
+
print(f'\n[Speed]: {gen_tokens / (time.time() - st):.2f} tokens/s') if args.show_speed else print()
|
| 128 |
+
return response
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def chat_api(client, messages, tools, args, stream=True):
|
| 132 |
+
response = client.chat.completions.create(
|
| 133 |
+
model=args.api_model, messages=messages, tools=tools,
|
| 134 |
+
stream=stream, temperature=args.temperature,
|
| 135 |
+
max_tokens=8192, top_p=args.top_p
|
| 136 |
+
)
|
| 137 |
+
if not stream:
|
| 138 |
+
choice = response.choices[0]
|
| 139 |
+
content = choice.message.content or ""
|
| 140 |
+
tool_calls = choice.message.tool_calls
|
| 141 |
+
if not tool_calls:
|
| 142 |
+
tool_calls = parse_tool_call_from_text(content)
|
| 143 |
+
print(f'🧠: {content}')
|
| 144 |
+
return content, tool_calls
|
| 145 |
+
print('🧠: ', end='', flush=True)
|
| 146 |
+
content, tool_calls = "", None
|
| 147 |
+
for chunk in response:
|
| 148 |
+
delta = chunk.choices[0].delta
|
| 149 |
+
if delta.content:
|
| 150 |
+
print(delta.content, end="", flush=True)
|
| 151 |
+
content += delta.content
|
| 152 |
+
if delta.tool_calls:
|
| 153 |
+
if tool_calls is None:
|
| 154 |
+
tool_calls = []
|
| 155 |
+
for tc_chunk in delta.tool_calls:
|
| 156 |
+
idx = tc_chunk.index if tc_chunk.index is not None else len(tool_calls)
|
| 157 |
+
while len(tool_calls) <= idx:
|
| 158 |
+
tool_calls.append({
|
| 159 |
+
"id": "",
|
| 160 |
+
"function": {"name": "", "arguments": ""}
|
| 161 |
+
})
|
| 162 |
+
if tc_chunk.id:
|
| 163 |
+
tool_calls[idx]["id"] += tc_chunk.id
|
| 164 |
+
if tc_chunk.function:
|
| 165 |
+
if tc_chunk.function.name:
|
| 166 |
+
tool_calls[idx]["function"]["name"] += tc_chunk.function.name
|
| 167 |
+
if tc_chunk.function.arguments:
|
| 168 |
+
tool_calls[idx]["function"]["arguments"] += tc_chunk.function.arguments
|
| 169 |
+
print()
|
| 170 |
+
if not tool_calls:
|
| 171 |
+
tool_calls = parse_tool_call_from_text(content)
|
| 172 |
+
return content, tool_calls
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def run_case(prompt, tools, args, model=None, tokenizer=None, client=None):
|
| 176 |
+
messages = [{"role": "user", "content": prompt}]
|
| 177 |
+
while True:
|
| 178 |
+
if args.backend == 'local':
|
| 179 |
+
content = generate(model, tokenizer, messages, tools, args)
|
| 180 |
+
tool_calls = parse_tool_calls(content)
|
| 181 |
+
else:
|
| 182 |
+
content, tool_calls = chat_api(client, messages, tools, args, stream=bool(args.stream))
|
| 183 |
+
if not tool_calls:
|
| 184 |
+
break
|
| 185 |
+
tool_calls = [{
|
| 186 |
+
"id": tc.id if hasattr(tc, 'id') else tc.get("id", ""),
|
| 187 |
+
"name": tc.function.name if hasattr(tc, 'function') else tc["function"]["name"],
|
| 188 |
+
"arguments": tc.function.arguments if hasattr(tc, 'function') else tc["function"]["arguments"]
|
| 189 |
+
} for tc in tool_calls] if args.backend == 'api' else tool_calls
|
| 190 |
+
messages.append({"role": "assistant", "content": content} if args.backend == 'local' else {"role": "assistant", "content": content, "tool_calls": [{"id": tc["id"], "type": "function", "function": {"name": tc["name"], "arguments": tc["arguments"]}} for tc in tool_calls]})
|
| 191 |
+
for tc in tool_calls:
|
| 192 |
+
name = tc["name"]
|
| 193 |
+
arguments = tc["arguments"]
|
| 194 |
+
print(f'📞 [Tool Calling]: {name} | args={arguments}')
|
| 195 |
+
result = execute_tool(tc if args.backend == 'local' else name, arguments)
|
| 196 |
+
print(f'✅ [Tool Called]: {json.dumps(result, ensure_ascii=False)}')
|
| 197 |
+
messages.append({"role": "tool", "content": json.dumps(result, ensure_ascii=False)} if args.backend == 'local' else {"role": "tool", "content": json.dumps(result, ensure_ascii=False), "tool_call_id": tc["id"]})
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def main():
|
| 201 |
+
parser = argparse.ArgumentParser(description="MiniMind ToolCall评测")
|
| 202 |
+
parser.add_argument('--backend', default='local', choices=['local', 'api'], type=str, help="推理后端(local=本地模型,api=OpenAI兼容接口)")
|
| 203 |
+
parser.add_argument('--load_from', default='../model', type=str, help="模型加载路径(model=原生torch权重,其他路径=transformers格式)")
|
| 204 |
+
parser.add_argument('--save_dir', default='../out', type=str, help="模型权重目录")
|
| 205 |
+
parser.add_argument('--weight', default='full_sft', type=str, help="权重名称前缀(pretrain, full_sft, rlhf, reason, ppo_actor, grpo, spo)")
|
| 206 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 207 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 208 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 209 |
+
parser.add_argument('--max_new_tokens', default=512, type=int, help="最大生成长度")
|
| 210 |
+
parser.add_argument('--temperature', default=0.9, type=float, help="生成温度,控制随机性(0-1,越大越随机)")
|
| 211 |
+
parser.add_argument('--top_p', default=0.9, type=float, help="nucleus采样阈值(0-1)")
|
| 212 |
+
parser.add_argument('--show_speed', default=0, type=int, help="显示decode速度(tokens/s)")
|
| 213 |
+
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', type=str, help="运行设备")
|
| 214 |
+
parser.add_argument('--api_base_url', default="http://localhost:11434/v1", type=str, help="OpenAI兼容接口的base_url")
|
| 215 |
+
parser.add_argument('--api_key', default='sk-123', type=str, help="OpenAI兼容接口的api_key")
|
| 216 |
+
parser.add_argument('--api_model', default='jingyaogong/minimind-3:latest', type=str, help="API请求时使用的模型名称")
|
| 217 |
+
parser.add_argument('--stream', default=1, type=int, help="API模式下是否流式输出(0=否,1=是)")
|
| 218 |
+
args = parser.parse_args()
|
| 219 |
+
|
| 220 |
+
model = tokenizer = client = None
|
| 221 |
+
if args.backend == 'local': model, tokenizer = init_model(args)
|
| 222 |
+
else: client = OpenAI(api_key=args.api_key, base_url=args.api_base_url)
|
| 223 |
+
|
| 224 |
+
input_mode = int(input('[0] 自动测试\n[1] 手动输入\n'))
|
| 225 |
+
|
| 226 |
+
cases = [{"prompt": case["prompt"], "tools": get_tools(case["tools"]), "tool_names": case["tools"]} for case in TEST_CASES] if input_mode == 0 else iter(lambda: {"prompt": input('💬: '), "tools": TOOLS, "tool_names": [t["function"]["name"] for t in TOOLS]}, {"prompt": "", "tools": TOOLS, "tool_names": []})
|
| 227 |
+
for case in cases:
|
| 228 |
+
if not case["prompt"]: break
|
| 229 |
+
setup_seed(random.randint(0, 31415926))
|
| 230 |
+
if input_mode == 0:
|
| 231 |
+
print(f'📦 可用工具: {case["tool_names"]}\n')
|
| 232 |
+
print(f'💬: {case["prompt"]}')
|
| 233 |
+
run_case(case["prompt"], case["tools"], args, model=model, tokenizer=tokenizer, client=client)
|
| 234 |
+
print('\n' + '-' * 50 + '\n')
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
if __name__ == "__main__":
|
| 238 |
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import argparse
|
| 3 |
+
import os
|
| 4 |
+
import warnings
|
| 5 |
+
import torch
|
| 6 |
+
import random
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
|
| 9 |
+
from omni.models import MiniMindVLM, VLMConfig
|
| 10 |
+
from omni.utils import setup_seed, get_vlm_model_params
|
| 11 |
+
warnings.filterwarnings('ignore')
|
| 12 |
+
|
| 13 |
+
def init_model(args):
|
| 14 |
+
tokenizer = AutoTokenizer.from_pretrained(args.load_from, trust_remote_code=True)
|
| 15 |
+
if 'model' in args.load_from:
|
| 16 |
+
moe_suffix = '_moe' if args.use_moe else ''
|
| 17 |
+
ckp = f'./{args.save_dir}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
|
| 18 |
+
model = MiniMindVLM(
|
| 19 |
+
VLMConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe)),
|
| 20 |
+
vision_model_path="./model/siglip2-base-p32-256-ve"
|
| 21 |
+
)
|
| 22 |
+
state_dict = torch.load(ckp, map_location=args.device)
|
| 23 |
+
model.load_state_dict({k: v for k, v in state_dict.items() if 'mask' not in k}, strict=False)
|
| 24 |
+
else:
|
| 25 |
+
model = AutoModelForCausalLM.from_pretrained(args.load_from, trust_remote_code=True)
|
| 26 |
+
model.vision_encoder, model.processor = MiniMindVLM.get_vision_model("./model/siglip2-base-p32-256-ve")
|
| 27 |
+
get_vlm_model_params(model, model.config)
|
| 28 |
+
model = model.eval()
|
| 29 |
+
if "cuda" in args.device: model = model.half()
|
| 30 |
+
return model.to(args.device), tokenizer, model.processor
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main():
|
| 34 |
+
parser = argparse.ArgumentParser(description="MiniMind-V Chat")
|
| 35 |
+
parser.add_argument('--load_from', default='model', type=str, help="模型加载路径(model=原生torch权重,其他路径=transformers格式)")
|
| 36 |
+
parser.add_argument('--save_dir', default='out', type=str, help="模型权重目录")
|
| 37 |
+
parser.add_argument('--weight', default='sft_vlm', type=str, help="权重名称前缀(pretrain_vlm, sft_vlm)")
|
| 38 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 39 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 40 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 41 |
+
parser.add_argument('--max_new_tokens', default=512, type=int, help="最大生成长度")
|
| 42 |
+
parser.add_argument('--temperature', default=0.7, type=float, help="生成温度,控制随机性(0-1,越大越随机)")
|
| 43 |
+
parser.add_argument('--top_p', default=0.85, type=float, help="nucleus采样阈值(0-1)")
|
| 44 |
+
parser.add_argument('--image_dir', default='./dataset/eval_images/', type=str, help="测试图像目录")
|
| 45 |
+
parser.add_argument('--show_speed', default=1, type=int, help="显示decode速度(tokens/s)")
|
| 46 |
+
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', type=str, help="运行设备")
|
| 47 |
+
parser.add_argument('--open_thinking', default=0, type=int, help="是否开启自适应思考(0=否,1=是)")
|
| 48 |
+
args = parser.parse_args()
|
| 49 |
+
|
| 50 |
+
model, tokenizer, preprocess = init_model(args)
|
| 51 |
+
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 52 |
+
# 自动测试image_dir中的所有图像
|
| 53 |
+
prompt = "<image>\n请描述这张图中的主要物体和场景。"
|
| 54 |
+
for image_file in sorted(os.listdir(args.image_dir)):
|
| 55 |
+
if image_file.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):
|
| 56 |
+
setup_seed(random.randint(1, 31415926))
|
| 57 |
+
image_path = os.path.join(args.image_dir, image_file)
|
| 58 |
+
image = Image.open(image_path).convert('RGB')
|
| 59 |
+
pixel_values = {k: v.to(args.device) for k, v in MiniMindVLM.image2tensor(image, preprocess).items()}
|
| 60 |
+
|
| 61 |
+
messages = [{"role": "user", "content": prompt.replace('<image>', model.config.image_special_token * model.config.image_token_len)}]
|
| 62 |
+
inputs_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, open_thinking=bool(args.open_thinking))
|
| 63 |
+
inputs = tokenizer(inputs_text, return_tensors="pt", truncation=True).to(args.device)
|
| 64 |
+
|
| 65 |
+
print(f'[图像]: {image_file}')
|
| 66 |
+
print(f"💬: {repr(prompt)}")
|
| 67 |
+
print('🤖: ', end='')
|
| 68 |
+
st = time.time()
|
| 69 |
+
generated_ids = model.generate(
|
| 70 |
+
inputs=inputs["input_ids"], attention_mask=inputs["attention_mask"],
|
| 71 |
+
max_new_tokens=args.max_new_tokens, do_sample=True, streamer=streamer,
|
| 72 |
+
pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id,
|
| 73 |
+
top_p=args.top_p, temperature=args.temperature, pixel_values=pixel_values
|
| 74 |
+
)
|
| 75 |
+
gen_tokens = len(generated_ids[0]) - len(inputs["input_ids"][0])
|
| 76 |
+
print(f'\n[Speed]: {gen_tokens / (time.time() - st):.2f} tokens/s\n\n') if args.show_speed else print('\n\n')
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
main()
|
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse, os, sys, json, time, math, torch, threading, queue, base64, io, logging, contextlib
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 5 |
+
from omni.models import MiniMindOmni
|
| 6 |
+
from omni.serve.realtime import RealtimeSession
|
| 7 |
+
from omni.utils import log_model_params
|
| 8 |
+
logging.getLogger().setLevel(logging.ERROR)
|
| 9 |
+
|
| 10 |
+
M = {} # model / tokenizer / device / mimi / asr / cfg
|
| 11 |
+
V = {} # voice_name -> {ref_codes, spk_emb}
|
| 12 |
+
V_builtin, V_unseen, V_manual = [], [], []
|
| 13 |
+
MODEL_LOCK = threading.Lock()
|
| 14 |
+
SAMPLES_PER_FRAME = 1920
|
| 15 |
+
REF_FRAMES = 300
|
| 16 |
+
CLONE_VOICE = 'voice_clone'
|
| 17 |
+
CLONE_FILE = 'voice_clone.pt'
|
| 18 |
+
|
| 19 |
+
# -------- helpers --------
|
| 20 |
+
def sse(d): return f"data: {json.dumps(d)}\n\n"
|
| 21 |
+
|
| 22 |
+
def scan_hf_models(base_dir):
|
| 23 |
+
models = {}
|
| 24 |
+
base_dir = os.path.abspath(base_dir)
|
| 25 |
+
for d in sorted(os.listdir(base_dir), reverse=True):
|
| 26 |
+
full_path = os.path.join(base_dir, d)
|
| 27 |
+
if not os.path.isdir(full_path) or d.startswith('.') or d.startswith('_'):
|
| 28 |
+
continue
|
| 29 |
+
files = set(os.listdir(full_path))
|
| 30 |
+
has_model = bool(files & {'pytorch_model.bin', 'model.safetensors', 'pytorch_model.bin.index.json', 'model.safetensors.index.json'})
|
| 31 |
+
if has_model:
|
| 32 |
+
models[d] = full_path
|
| 33 |
+
return models
|
| 34 |
+
|
| 35 |
+
def asr_run(samples):
|
| 36 |
+
from funasr.utils.postprocess_utils import rich_transcription_postprocess
|
| 37 |
+
r = M['asr'].generate(input=samples, cache={}, language='auto', use_itn=True)
|
| 38 |
+
return rich_transcription_postprocess(r[0]['text']).strip() if r else ''
|
| 39 |
+
|
| 40 |
+
def prep_audio(samples):
|
| 41 |
+
m = M['model']
|
| 42 |
+
proc = m.audio_processor(samples, sampling_rate=16000, return_tensors="pt", return_attention_mask=True)
|
| 43 |
+
mel = proc.input_features.squeeze(0).unsqueeze(0).to(M['device'])
|
| 44 |
+
vlen = proc.attention_mask.sum().item()
|
| 45 |
+
prompt = m.config.audio_special_token * (vlen or 1)
|
| 46 |
+
return mel, torch.tensor([vlen], device=M['device']), prompt
|
| 47 |
+
|
| 48 |
+
def prep_image(b64):
|
| 49 |
+
img = Image.open(io.BytesIO(base64.b64decode(b64))).convert('RGB')
|
| 50 |
+
return {k: v.to(M['device']) for k, v in M['model'].vision_processor(images=img, return_tensors="pt").items()}
|
| 51 |
+
|
| 52 |
+
def build_ids(prompt, history):
|
| 53 |
+
tok, dev, n = M['tokenizer'], M['device'], M['cfg'].max_history_turns
|
| 54 |
+
hist = history[-n:] if n > 0 else []
|
| 55 |
+
msgs = hist + [{"role": "user", "content": prompt}]
|
| 56 |
+
t = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
| 57 |
+
return torch.tensor(tok(t).data['input_ids'], dtype=torch.long, device=dev)[None, ...]
|
| 58 |
+
|
| 59 |
+
def _mimi_decode(frames):
|
| 60 |
+
codes = [f for f in frames if f and len(f) == 8]
|
| 61 |
+
if not codes or not M['mimi']: return None
|
| 62 |
+
mc = torch.tensor(codes, dtype=torch.long, device=M['device']).T.unsqueeze(0)
|
| 63 |
+
mc = torch.where(mc >= 2049, torch.zeros_like(mc), mc)
|
| 64 |
+
with torch.no_grad():
|
| 65 |
+
au = M['mimi'].decode(mc).audio_values.squeeze().cpu().numpy()
|
| 66 |
+
return au, mc.shape[-1]
|
| 67 |
+
|
| 68 |
+
def pcm_bytes(frames, ov):
|
| 69 |
+
r = _mimi_decode(frames)
|
| 70 |
+
if r is None: return None
|
| 71 |
+
au, T = r
|
| 72 |
+
if ov > 0: au = au[int(ov * len(au) / T):]
|
| 73 |
+
return (au * 32767).astype('int16').tobytes()
|
| 74 |
+
|
| 75 |
+
def stream_pcm(frames, flush=False):
|
| 76 |
+
"""yield (pcm_bytes,) on chunk boundaries or on final flush."""
|
| 77 |
+
if not M['mimi']: return
|
| 78 |
+
cf, ov_max, n = M['cfg'].audio_chunk_frames, M['cfg'].audio_overlap, len(frames)
|
| 79 |
+
if not flush and n >= cf and n % cf == 0:
|
| 80 |
+
ov = min(ov_max, n - cf)
|
| 81 |
+
p = pcm_bytes(frames[-(cf + ov):], ov)
|
| 82 |
+
if p: yield p
|
| 83 |
+
elif flush:
|
| 84 |
+
rem = n % cf
|
| 85 |
+
if rem:
|
| 86 |
+
ov = min(ov_max, n - rem)
|
| 87 |
+
p = pcm_bytes(frames[-(rem + ov):], ov)
|
| 88 |
+
if p: yield p
|
| 89 |
+
|
| 90 |
+
def voice_args(name):
|
| 91 |
+
if name and name != 'default' and name in V:
|
| 92 |
+
v = V[name]
|
| 93 |
+
dev = M['device']
|
| 94 |
+
rc = v['ref_codes'].unsqueeze(0).to(dev)
|
| 95 |
+
se = v['spk_emb'].half().unsqueeze(0).to(dev) if 'spk_emb' in v else None
|
| 96 |
+
return {'ref_codes': rc, 'spk_emb': se}
|
| 97 |
+
return {}
|
| 98 |
+
|
| 99 |
+
def register_voice(name, value, group='manual'):
|
| 100 |
+
V[name] = value
|
| 101 |
+
groups = {'builtin': V_builtin, 'unseen': V_unseen, 'manual': V_manual}
|
| 102 |
+
dst = groups[group]
|
| 103 |
+
if name not in dst:
|
| 104 |
+
dst.append(name)
|
| 105 |
+
for k, lst in groups.items():
|
| 106 |
+
if k != group and name in lst:
|
| 107 |
+
lst.remove(name)
|
| 108 |
+
|
| 109 |
+
def clone_voice_path():
|
| 110 |
+
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'speaker', CLONE_FILE)
|
| 111 |
+
|
| 112 |
+
def delete_manual_voice(name):
|
| 113 |
+
if name not in V_manual:
|
| 114 |
+
raise RuntimeError('只能删除手动克隆的音色')
|
| 115 |
+
out_path = clone_voice_path()
|
| 116 |
+
saved = torch.load(out_path, map_location='cpu') if os.path.exists(out_path) else {}
|
| 117 |
+
if name in saved:
|
| 118 |
+
saved.pop(name)
|
| 119 |
+
torch.save(saved, out_path)
|
| 120 |
+
V.pop(name, None)
|
| 121 |
+
if name in V_manual:
|
| 122 |
+
V_manual.remove(name)
|
| 123 |
+
|
| 124 |
+
def normalize_voice_name(name):
|
| 125 |
+
name = ' '.join(str(name or '').split())
|
| 126 |
+
if not name:
|
| 127 |
+
name = CLONE_VOICE
|
| 128 |
+
if len(name) > 24:
|
| 129 |
+
raise RuntimeError('音色名太长,建议控制在 24 个字以内')
|
| 130 |
+
if name.lower() == 'default':
|
| 131 |
+
raise RuntimeError('default 是保留名称,请换一个')
|
| 132 |
+
if name in V_builtin or name in V_unseen:
|
| 133 |
+
raise RuntimeError('该名称已被现有音色占用,请换一个')
|
| 134 |
+
return name
|
| 135 |
+
|
| 136 |
+
def validate_clone_audio(w16):
|
| 137 |
+
if w16.numel() < int(16000 * 1.8):
|
| 138 |
+
raise RuntimeError('录音太短,请把整句话读完')
|
| 139 |
+
peak = w16.abs().max().item()
|
| 140 |
+
frame, hop = 800, 400
|
| 141 |
+
if w16.numel() >= frame:
|
| 142 |
+
rms = w16.unfold(0, frame, hop).pow(2).mean(dim=1).sqrt().cpu().numpy()
|
| 143 |
+
else:
|
| 144 |
+
rms = np.array([w16.pow(2).mean().sqrt().item()])
|
| 145 |
+
hi = float(np.quantile(rms, 0.95))
|
| 146 |
+
lo = float(np.quantile(rms, 0.2))
|
| 147 |
+
if hi < 0.008:
|
| 148 |
+
raise RuntimeError('录音太轻,请靠近麦克风一点')
|
| 149 |
+
if hi > 0 and lo / hi > 0.45:
|
| 150 |
+
raise RuntimeError('环境噪声太大,请换安静一点的环境')
|
| 151 |
+
if peak > 0.995:
|
| 152 |
+
raise RuntimeError('录音有爆音,请离麦克风远一点')
|
| 153 |
+
|
| 154 |
+
def build_clone_voice(audio_b64):
|
| 155 |
+
if M.get('mimi') is None or M.get('campplus') is None or M.get('mel_fn') is None:
|
| 156 |
+
raise RuntimeError('Mimi 或 CAM++ 未加载')
|
| 157 |
+
from pydub import AudioSegment
|
| 158 |
+
seg = AudioSegment.from_file(io.BytesIO(base64.b64decode(audio_b64))).set_channels(1).set_sample_width(2)
|
| 159 |
+
if len(seg) < 1000:
|
| 160 |
+
raise RuntimeError('录音太短,至少读 1 秒')
|
| 161 |
+
try:
|
| 162 |
+
seg = seg.speedup(playback_speed=1.5, chunk_size=150, crossfade=25)
|
| 163 |
+
except Exception:
|
| 164 |
+
seg = seg.speedup(playback_speed=1.5)
|
| 165 |
+
seg24 = seg.set_frame_rate(24000)
|
| 166 |
+
seg16 = seg.set_frame_rate(16000)
|
| 167 |
+
w24 = torch.tensor(np.frombuffer(seg24.raw_data, dtype=np.int16).astype(np.float32) / 32768.0)
|
| 168 |
+
w16 = torch.tensor(np.frombuffer(seg16.raw_data, dtype=np.int16).astype(np.float32) / 32768.0)
|
| 169 |
+
validate_clone_audio(w16)
|
| 170 |
+
mimi_dev = next(M['mimi'].parameters()).device
|
| 171 |
+
mimi_dtype = torch.float16 if mimi_dev.type != 'cpu' else torch.float32
|
| 172 |
+
with torch.inference_mode():
|
| 173 |
+
t = w24.unsqueeze(0).unsqueeze(0).to(device=mimi_dev, dtype=mimi_dtype)
|
| 174 |
+
codes = M['mimi'].encode(t).audio_codes
|
| 175 |
+
nf = math.ceil(w24.shape[-1] / SAMPLES_PER_FRAME)
|
| 176 |
+
ref_codes = codes[0, :8, :nf].cpu()[:, :min(nf, REF_FRAMES)]
|
| 177 |
+
with torch.no_grad():
|
| 178 |
+
mel = M['mel_fn'](w16.unsqueeze(0).to(M['device']))
|
| 179 |
+
feat = mel.clamp(min=1e-10).log().transpose(1, 2)
|
| 180 |
+
feat = feat - feat.mean(dim=1, keepdim=True)
|
| 181 |
+
spk_emb = M['campplus'](feat).squeeze(0).cpu()
|
| 182 |
+
return {'ref_codes': ref_codes, 'spk_emb': spk_emb}
|
| 183 |
+
|
| 184 |
+
def run_generate(x, audio_inputs, audio_lens, pixel_values, **kw):
|
| 185 |
+
with MODEL_LOCK, torch.no_grad():
|
| 186 |
+
yield from M['model'].generate(
|
| 187 |
+
x, M['tokenizer'].eos_token_id, stream=True, return_audio_codes=True,
|
| 188 |
+
audio_inputs=audio_inputs, audio_lens=audio_lens, pixel_values=pixel_values, **kw)
|
| 189 |
+
|
| 190 |
+
def load_main_model(model_path, model_name):
|
| 191 |
+
with MODEL_LOCK:
|
| 192 |
+
[sys.modules.pop(k) for k in list(sys.modules) if 'transformers_modules' in k]
|
| 193 |
+
M.pop('model', None); M.pop('tokenizer', None)
|
| 194 |
+
if torch.cuda.is_available(): torch.cuda.empty_cache()
|
| 195 |
+
tok = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 196 |
+
m = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True)
|
| 197 |
+
vision_encoder, vision_processor = MiniMindOmni.load_vision('../model/siglip2-base-p32-256-ve')
|
| 198 |
+
audio_encoder, audio_processor = MiniMindOmni.load_sensevoice('../model/SenseVoiceSmall')
|
| 199 |
+
object.__setattr__(m, 'vision_encoder', vision_encoder)
|
| 200 |
+
object.__setattr__(m, 'vision_processor', vision_processor)
|
| 201 |
+
object.__setattr__(m, 'audio_encoder', audio_encoder)
|
| 202 |
+
object.__setattr__(m, 'audio_processor', audio_processor)
|
| 203 |
+
m = m.half().eval().to(M['device'])
|
| 204 |
+
if m.audio_encoder: m.audio_encoder.to(M['device'])
|
| 205 |
+
if m.vision_encoder: m.vision_encoder.to(M['device'])
|
| 206 |
+
M['tokenizer'], M['model'], M['model_name'] = tok, m, model_name
|
| 207 |
+
params = sum(p.numel() for p in m.parameters()) / 1e6
|
| 208 |
+
print(f'Loaded model: {model_name} ({params:.2f}M)')
|
| 209 |
+
return round(params, 2)
|
| 210 |
+
|
| 211 |
+
def prepare_turn(text, samples, image_b64, do_asr_for_image):
|
| 212 |
+
"""返回 (audio_inputs, audio_lens, pixel_values, prompt_for_model, user_text_for_history, asr_thread, asr_result)"""
|
| 213 |
+
audio_inputs = audio_lens = pixel_values = None
|
| 214 |
+
prompt = text or ''
|
| 215 |
+
user_text = text or ''
|
| 216 |
+
asr_thread, asr_result = None, [None]
|
| 217 |
+
if samples is not None:
|
| 218 |
+
from pydub import AudioSegment
|
| 219 |
+
if image_b64 and do_asr_for_image:
|
| 220 |
+
user_text = asr_run(samples)
|
| 221 |
+
prompt = user_text
|
| 222 |
+
else:
|
| 223 |
+
audio_inputs, audio_lens, prompt = prep_audio(samples)
|
| 224 |
+
if M['cfg'].max_history_turns > 0:
|
| 225 |
+
sa = samples.copy()
|
| 226 |
+
def _a(): asr_result[0] = asr_run(sa)
|
| 227 |
+
asr_thread = threading.Thread(target=_a); asr_thread.start()
|
| 228 |
+
if image_b64:
|
| 229 |
+
pixel_values = prep_image(image_b64)
|
| 230 |
+
m = M['model']
|
| 231 |
+
prompt = (prompt + "\n\n" if prompt else "") + "请描述这张图片\n\n" + m.config.image_special_token * m.config.image_token_len
|
| 232 |
+
return audio_inputs, audio_lens, pixel_values, prompt, user_text, asr_thread, asr_result
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# -------- web app (lazy: requires flask/flask_sock at runtime) --------
|
| 236 |
+
def init_web_app():
|
| 237 |
+
from flask import Flask, request, Response, send_from_directory
|
| 238 |
+
from flask_cors import CORS
|
| 239 |
+
from flask_sock import Sock
|
| 240 |
+
|
| 241 |
+
app = Flask(__name__, static_folder='.')
|
| 242 |
+
CORS(app)
|
| 243 |
+
sock = Sock(app)
|
| 244 |
+
|
| 245 |
+
@app.route('/')
|
| 246 |
+
def index(): return send_from_directory('.', 'web_demo.html')
|
| 247 |
+
@app.route('/call')
|
| 248 |
+
def call_page(): return send_from_directory('.', 'web_demo.html')
|
| 249 |
+
|
| 250 |
+
@app.route('/voices')
|
| 251 |
+
def get_voices():
|
| 252 |
+
return json.dumps({'builtin': sorted(V_builtin), 'unseen': sorted(V_unseen), 'manual': sorted(V_manual)})
|
| 253 |
+
|
| 254 |
+
@app.route('/models')
|
| 255 |
+
def get_models():
|
| 256 |
+
return json.dumps({'models': list(M.get('models', {}).keys()), 'current': M.get('model_name')})
|
| 257 |
+
|
| 258 |
+
@app.route('/switch_model', methods=['POST'])
|
| 259 |
+
def switch_model():
|
| 260 |
+
name = (request.json or {}).get('name')
|
| 261 |
+
if name not in M.get('models', {}):
|
| 262 |
+
return Response(json.dumps({'ok': False, 'error': 'unknown model'}), status=400, mimetype='application/json')
|
| 263 |
+
try:
|
| 264 |
+
params = load_main_model(M['models'][name], name)
|
| 265 |
+
return Response(json.dumps({'ok': True, 'model': name, 'params': params}), mimetype='application/json')
|
| 266 |
+
except Exception as e:
|
| 267 |
+
return Response(json.dumps({'ok': False, 'error': str(e)}), status=500, mimetype='application/json')
|
| 268 |
+
|
| 269 |
+
@app.route('/clone_voice', methods=['POST'])
|
| 270 |
+
def clone_voice():
|
| 271 |
+
d = request.json or {}
|
| 272 |
+
if not d.get('audio'):
|
| 273 |
+
return Response(json.dumps({'ok': False, 'error': 'missing audio'}), status=400, mimetype='application/json')
|
| 274 |
+
try:
|
| 275 |
+
name = normalize_voice_name(d.get('name'))
|
| 276 |
+
value = build_clone_voice(d['audio'])
|
| 277 |
+
out_path = clone_voice_path()
|
| 278 |
+
saved = torch.load(out_path, map_location='cpu') if os.path.exists(out_path) else {}
|
| 279 |
+
saved[name] = value
|
| 280 |
+
torch.save(saved, out_path)
|
| 281 |
+
register_voice(name, value, group='manual')
|
| 282 |
+
return Response(json.dumps({'ok': True, 'voice': name, 'path': './model/speaker/' + CLONE_FILE}), mimetype='application/json')
|
| 283 |
+
except Exception as e:
|
| 284 |
+
return Response(json.dumps({'ok': False, 'error': str(e)}), status=500, mimetype='application/json')
|
| 285 |
+
|
| 286 |
+
@app.route('/delete_voice', methods=['POST'])
|
| 287 |
+
def delete_voice():
|
| 288 |
+
d = request.json or {}
|
| 289 |
+
name = ' '.join(str(d.get('name') or '').split())
|
| 290 |
+
if not name:
|
| 291 |
+
return Response(json.dumps({'ok': False, 'error': 'missing name'}), status=400, mimetype='application/json')
|
| 292 |
+
try:
|
| 293 |
+
delete_manual_voice(name)
|
| 294 |
+
return Response(json.dumps({'ok': True, 'voice': name}), mimetype='application/json')
|
| 295 |
+
except Exception as e:
|
| 296 |
+
return Response(json.dumps({'ok': False, 'error': str(e)}), status=500, mimetype='application/json')
|
| 297 |
+
|
| 298 |
+
@app.route('/chat', methods=['POST'])
|
| 299 |
+
def chat():
|
| 300 |
+
d = request.json
|
| 301 |
+
history = d.get('history', [])
|
| 302 |
+
samples = None
|
| 303 |
+
if d.get('audio'):
|
| 304 |
+
seg = AudioSegment.from_file(io.BytesIO(base64.b64decode(d['audio']))).set_frame_rate(16000).set_channels(1).set_sample_width(2)
|
| 305 |
+
samples = np.frombuffer(seg.raw_data, dtype=np.int16).astype(np.float32) / 32768.0
|
| 306 |
+
va = voice_args(d.get('voice', 'default'))
|
| 307 |
+
|
| 308 |
+
def gen():
|
| 309 |
+
audio_inputs, audio_lens, pixel_values, prompt, user_text, asr_th, asr_res = prepare_turn(
|
| 310 |
+
d.get('text', ''), samples, d.get('image'), do_asr_for_image=True)
|
| 311 |
+
x = build_ids(prompt, history)
|
| 312 |
+
asr_sent = False
|
| 313 |
+
if user_text and samples is not None and d.get('image'):
|
| 314 |
+
yield sse({'type': 'user_prompt', 'content': user_text}); asr_sent = True
|
| 315 |
+
frames, text_ttft, audio_ttft = [], None, None
|
| 316 |
+
t0 = time.time(); hi = 0
|
| 317 |
+
for y, af in run_generate(x, audio_inputs, audio_lens, pixel_values,
|
| 318 |
+
max_new_tokens=d.get('max_tokens', 512),
|
| 319 |
+
temperature=d.get('temperature', 1), top_p=0.85, **va):
|
| 320 |
+
if not asr_sent and asr_th and not asr_th.is_alive():
|
| 321 |
+
asr_th.join()
|
| 322 |
+
if asr_res[0]: yield sse({'type': 'user_prompt', 'content': asr_res[0]})
|
| 323 |
+
asr_sent = True
|
| 324 |
+
if y is not None:
|
| 325 |
+
if text_ttft is None:
|
| 326 |
+
text_ttft = (time.time() - t0) * 1000
|
| 327 |
+
yield sse({'type': 'ttft', 'text_ttft': round(text_ttft, 1)})
|
| 328 |
+
ans = M['tokenizer'].decode(y[0].tolist(), skip_special_tokens=True)
|
| 329 |
+
if ans and ans[-1] != '\ufffd' and len(ans) > hi:
|
| 330 |
+
yield sse({'type': 'text', 'content': ans[hi:]}); hi = len(ans)
|
| 331 |
+
if af:
|
| 332 |
+
if audio_ttft is None:
|
| 333 |
+
audio_ttft = (time.time() - t0) * 1000
|
| 334 |
+
yield sse({'type': 'ttft', 'audio_ttft': round(audio_ttft, 1)})
|
| 335 |
+
frames.append(af)
|
| 336 |
+
for pcm in stream_pcm(frames):
|
| 337 |
+
b64 = base64.b64encode(pcm).decode()
|
| 338 |
+
for i in range(0, len(b64), 2000):
|
| 339 |
+
yield sse({'type': 'pcm', 'c': b64[i:i+2000], 'd': i+2000 >= len(b64)})
|
| 340 |
+
for pcm in stream_pcm(frames, flush=True):
|
| 341 |
+
b64 = base64.b64encode(pcm).decode()
|
| 342 |
+
for i in range(0, len(b64), 2000):
|
| 343 |
+
yield sse({'type': 'pcm', 'c': b64[i:i+2000], 'd': i+2000 >= len(b64)})
|
| 344 |
+
if not asr_sent:
|
| 345 |
+
if asr_th:
|
| 346 |
+
asr_th.join()
|
| 347 |
+
if asr_res[0]: yield sse({'type': 'user_prompt', 'content': asr_res[0]})
|
| 348 |
+
else:
|
| 349 |
+
yield sse({'type': 'user_prompt', 'content': prompt})
|
| 350 |
+
yield sse({'type': 'done'})
|
| 351 |
+
|
| 352 |
+
return Response(gen(), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
| 353 |
+
|
| 354 |
+
@sock.route('/ws/realtime')
|
| 355 |
+
def realtime(ws):
|
| 356 |
+
session = RealtimeSession(M['vad_path'])
|
| 357 |
+
q = queue.Queue(); alive = [True]; state = {'history': [], 'image': None}
|
| 358 |
+
n_hist = M['cfg'].max_history_turns
|
| 359 |
+
|
| 360 |
+
def push_audio(data):
|
| 361 |
+
return session.push_chunk(np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0)
|
| 362 |
+
|
| 363 |
+
def set_ctx(msg):
|
| 364 |
+
h = msg.get('history') or []
|
| 365 |
+
state['history'] = h[-n_hist:] if n_hist > 0 else []
|
| 366 |
+
if 'image' in msg: state['image'] = msg.get('image')
|
| 367 |
+
if 'voice' in msg: state['voice'] = msg.get('voice', 'default')
|
| 368 |
+
|
| 369 |
+
def poll_interrupt():
|
| 370 |
+
while True:
|
| 371 |
+
try: data = q.get_nowait()
|
| 372 |
+
except queue.Empty: return False
|
| 373 |
+
if isinstance(data, bytes):
|
| 374 |
+
if push_audio(data) == 'interrupt': return True
|
| 375 |
+
ws.send(json.dumps({'type': 'vad', 'speaking': session.speaking}))
|
| 376 |
+
else:
|
| 377 |
+
m = json.loads(data)
|
| 378 |
+
if m.get('type') == 'context': set_ctx(m)
|
| 379 |
+
elif m.get('type') in ('stop', 'end'):
|
| 380 |
+
if m['type'] == 'end': alive[0] = False
|
| 381 |
+
session.interrupt = True; return True
|
| 382 |
+
|
| 383 |
+
def recv_loop():
|
| 384 |
+
while alive[0]:
|
| 385 |
+
try:
|
| 386 |
+
data = ws.receive(timeout=1)
|
| 387 |
+
if data is None: alive[0] = False; break
|
| 388 |
+
q.put(data)
|
| 389 |
+
except: alive[0] = False; break
|
| 390 |
+
|
| 391 |
+
threading.Thread(target=recv_loop, daemon=True).start()
|
| 392 |
+
try:
|
| 393 |
+
while alive[0]:
|
| 394 |
+
try: data = q.get(timeout=0.05)
|
| 395 |
+
except queue.Empty: continue
|
| 396 |
+
if isinstance(data, str):
|
| 397 |
+
m = json.loads(data)
|
| 398 |
+
if m.get('type') == 'context': set_ctx(m)
|
| 399 |
+
elif m.get('type') == 'stop': session.interrupt = True
|
| 400 |
+
elif m.get('type') == 'end': break
|
| 401 |
+
continue
|
| 402 |
+
if session.generating:
|
| 403 |
+
push_audio(data); ws.send(json.dumps({'type': 'vad', 'speaking': session.speaking})); continue
|
| 404 |
+
status = push_audio(data)
|
| 405 |
+
ws.send(json.dumps({'type': 'vad', 'speaking': session.speaking}))
|
| 406 |
+
if status != 'speech_end': continue
|
| 407 |
+
|
| 408 |
+
session.generating = True
|
| 409 |
+
audio = session.get_audio()
|
| 410 |
+
ws.send(json.dumps({'type': 'generating'}))
|
| 411 |
+
audio_inputs, audio_lens, pixel_values, prompt, user_text, asr_th, asr_res = prepare_turn(
|
| 412 |
+
'', audio, state['image'], do_asr_for_image=True)
|
| 413 |
+
if state['image']: state['image'] = None
|
| 414 |
+
x = build_ids(prompt, state['history'])
|
| 415 |
+
va_rt = voice_args(state.get('voice', 'default'))
|
| 416 |
+
|
| 417 |
+
frames, full_text, interrupted = [], '', False
|
| 418 |
+
for y, af in run_generate(x, audio_inputs, audio_lens, pixel_values,
|
| 419 |
+
max_new_tokens=512, temperature=0.7, **va_rt):
|
| 420 |
+
if poll_interrupt() or session.interrupt: interrupted = True; break
|
| 421 |
+
if y is not None:
|
| 422 |
+
ans = M['tokenizer'].decode(y[0].tolist(), skip_special_tokens=True)
|
| 423 |
+
if ans and ans[-1] != '\ufffd' and len(ans) > len(full_text):
|
| 424 |
+
ws.send(json.dumps({'type': 'text', 'content': ans[len(full_text):]})); full_text = ans
|
| 425 |
+
if af:
|
| 426 |
+
frames.append(af)
|
| 427 |
+
for pcm in stream_pcm(frames):
|
| 428 |
+
ws.send(json.dumps({'type': 'pcm', 'data': base64.b64encode(pcm).decode()}))
|
| 429 |
+
if not interrupted:
|
| 430 |
+
for pcm in stream_pcm(frames, flush=True):
|
| 431 |
+
ws.send(json.dumps({'type': 'pcm', 'data': base64.b64encode(pcm).decode()}))
|
| 432 |
+
if asr_th:
|
| 433 |
+
asr_th.join(); user_text = asr_res[0] or user_text
|
| 434 |
+
if n_hist > 0:
|
| 435 |
+
if user_text: state['history'].append({'role': 'user', 'content': user_text})
|
| 436 |
+
if full_text: state['history'].append({'role': 'assistant', 'content': full_text})
|
| 437 |
+
state['history'] = state['history'][-n_hist:]
|
| 438 |
+
ws.send(json.dumps({'type': 'done', 'interrupted': interrupted or session.interrupt}))
|
| 439 |
+
session.generating = False; session.interrupt = False
|
| 440 |
+
finally:
|
| 441 |
+
alive[0] = False
|
| 442 |
+
|
| 443 |
+
return app
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def init_model(args):
|
| 447 |
+
M['cfg'] = args; M['device'] = args.device
|
| 448 |
+
with contextlib.redirect_stdout(io.StringIO()):
|
| 449 |
+
from funasr import AutoModel
|
| 450 |
+
from funasr.utils.postprocess_utils import rich_transcription_postprocess
|
| 451 |
+
M['asr'] = AutoModel(model='../model/SenseVoiceSmall', trust_remote_code=True, device=args.device, disable_update=True)
|
| 452 |
+
M['models'] = scan_hf_models(args.load_from)
|
| 453 |
+
if not M['models']:
|
| 454 |
+
raise RuntimeError(f"未在 {os.path.abspath(args.load_from)} 找到 transformers 模型")
|
| 455 |
+
model_name = next(iter(M['models']))
|
| 456 |
+
load_main_model(M['models'][model_name], model_name)
|
| 457 |
+
try:
|
| 458 |
+
import torchaudio
|
| 459 |
+
from transformers import MimiModel
|
| 460 |
+
M['mimi'] = MimiModel.from_pretrained('../model/mimi').eval().to(args.device)
|
| 461 |
+
if args.device != 'cpu': M['mimi'] = M['mimi'].half()
|
| 462 |
+
print('Mimi model loaded')
|
| 463 |
+
except Exception:
|
| 464 |
+
M['mimi'] = None
|
| 465 |
+
try:
|
| 466 |
+
from modelscope.models.audio.sv.DTDNN import CAMPPlus
|
| 467 |
+
M['campplus'] = CAMPPlus(feat_dim=80, embedding_size=192, growth_rate=32, bn_size=4,
|
| 468 |
+
init_channels=128, config_str='batchnorm-relu', memory_efficient=True)
|
| 469 |
+
sd = torch.load('../model/campplus/campplus_cn_common.pt', map_location='cpu')
|
| 470 |
+
M['campplus'].load_state_dict({k: v.float() for k, v in sd.items()})
|
| 471 |
+
M['campplus'] = M['campplus'].eval().to(args.device)
|
| 472 |
+
import torchaudio
|
| 473 |
+
M['mel_fn'] = torchaudio.transforms.MelSpectrogram(
|
| 474 |
+
sample_rate=16000, n_fft=512, win_length=400, hop_length=160,
|
| 475 |
+
n_mels=80, f_min=20, f_max=7600, norm='slaney', mel_scale='slaney',
|
| 476 |
+
).to(args.device)
|
| 477 |
+
print('CAM++ loaded')
|
| 478 |
+
except Exception as e:
|
| 479 |
+
M['campplus'], M['mel_fn'] = None, None
|
| 480 |
+
print(f'CAM++ load failed: {e}')
|
| 481 |
+
M['vad_path'] = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'vad', 'silero_vad.onnx')
|
| 482 |
+
spk_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'speaker')
|
| 483 |
+
for fn, group in [('voices.pt', 'builtin'), ('voices_unseen.pt', 'unseen'), (CLONE_FILE, 'manual')]:
|
| 484 |
+
fp = os.path.join(spk_dir, fn)
|
| 485 |
+
if os.path.exists(fp):
|
| 486 |
+
for speaker, v in torch.load(fp, map_location=args.device).items():
|
| 487 |
+
if speaker not in V or fn == CLONE_FILE:
|
| 488 |
+
register_voice(speaker, v, group=group)
|
| 489 |
+
if V: print(f'Loaded {len(V)} voices: builtin={sorted(V_builtin)}, unseen={sorted(V_unseen)}, manual={sorted(V_manual)}')
|
| 490 |
+
log_model_params(M['model'])
|
| 491 |
+
print('Warmup...')
|
| 492 |
+
with torch.no_grad():
|
| 493 |
+
ids = torch.tensor([[1, 2, 3]], device=args.device)
|
| 494 |
+
au = torch.full((1, 8, 3), 2049, dtype=torch.long, device=args.device)
|
| 495 |
+
M['model'].forward(torch.cat((au, ids.unsqueeze(1)), dim=1))
|
| 496 |
+
if M['model'].audio_encoder: M['model'].audio_encoder(torch.zeros(1, 100, 560, device=args.device), torch.tensor([100], device=args.device))
|
| 497 |
+
if M['mimi']: M['mimi'].decode(torch.zeros(1, 8, 1, dtype=torch.long, device=args.device))
|
| 498 |
+
print('Warmup done!')
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
if __name__ == '__main__':
|
| 502 |
+
p = argparse.ArgumentParser()
|
| 503 |
+
p.add_argument('--load_from', default='./', help='模型权重搜索目录;目录下可放多个 HF 格式模型,WebUI 会自动扫描并允许切换。')
|
| 504 |
+
p.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', help='推理设备;CUDA 可用时默认 cuda。显存不足或排查环境问题时可改为 cpu。')
|
| 505 |
+
p.add_argument('--port', default=7860, type=int, help='WebUI 服务端口;端口被占用或需要同时启动多个实例时调整。')
|
| 506 |
+
p.add_argument('--audio_chunk_frames', default=4, type=int, help='流式播放每次解码的 Mimi frame 数;默认 4 约 320ms。WebUI 播放卡顿时可调大到 8/12,低延迟优先时保持 4。')
|
| 507 |
+
p.add_argument('--audio_overlap', default=2, type=int, help='分块 Mimi 解码的重叠帧数;默认 2 用于缓解块边界断裂。一般不需要调整,边界杂音明显时可适当增大。')
|
| 508 |
+
p.add_argument('--max_history_turns', default=0, type=int, help='对话历史轮数;默认 0 不带历史以降低延迟和显存。需要多轮上下文时调大,但会增加 prefill 成本。')
|
| 509 |
+
args = p.parse_args()
|
| 510 |
+
init_model(args)
|
| 511 |
+
app = init_web_app()
|
| 512 |
+
app.run(host='0.0.0.0', port=args.port, threaded=True)
|
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
import torch
|
| 8 |
+
import warnings
|
| 9 |
+
import uvicorn
|
| 10 |
+
|
| 11 |
+
from threading import Thread
|
| 12 |
+
from queue import Queue
|
| 13 |
+
from fastapi import FastAPI, HTTPException
|
| 14 |
+
from fastapi.responses import StreamingResponse
|
| 15 |
+
from pydantic import BaseModel, Field
|
| 16 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
|
| 17 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 18 |
+
from omni.models.lora import apply_lora, load_lora
|
| 19 |
+
|
| 20 |
+
warnings.filterwarnings('ignore')
|
| 21 |
+
|
| 22 |
+
app = FastAPI()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def init_model(args):
|
| 26 |
+
tokenizer = AutoTokenizer.from_pretrained(args.load_from)
|
| 27 |
+
if 'model' in args.load_from:
|
| 28 |
+
moe_suffix = '_moe' if args.use_moe else ''
|
| 29 |
+
ckp = f'../{args.save_dir}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
|
| 30 |
+
model = MiniMindForCausalLM(MiniMindConfig(
|
| 31 |
+
hidden_size=args.hidden_size,
|
| 32 |
+
num_hidden_layers=args.num_hidden_layers,
|
| 33 |
+
max_seq_len=args.max_seq_len,
|
| 34 |
+
use_moe=bool(args.use_moe),
|
| 35 |
+
inference_rope_scaling=args.inference_rope_scaling
|
| 36 |
+
))
|
| 37 |
+
model.load_state_dict(torch.load(ckp, map_location=device), strict=True)
|
| 38 |
+
if args.lora_weight != 'None':
|
| 39 |
+
apply_lora(model)
|
| 40 |
+
load_lora(model, f'../{args.save_dir}/lora/{args.lora_weight}_{args.hidden_size}.pth')
|
| 41 |
+
else:
|
| 42 |
+
model = AutoModelForCausalLM.from_pretrained(args.load_from, trust_remote_code=True)
|
| 43 |
+
print(f'MiniMind模型参数量: {sum(p.numel() for p in model.parameters()) / 1e6:.2f} M(illion)')
|
| 44 |
+
return model.half().eval().to(device), tokenizer
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class ChatRequest(BaseModel):
|
| 48 |
+
model: str
|
| 49 |
+
messages: list
|
| 50 |
+
temperature: float = 0.7
|
| 51 |
+
top_p: float = 0.92
|
| 52 |
+
max_tokens: int = 8192
|
| 53 |
+
stream: bool = True
|
| 54 |
+
tools: list = Field(default_factory=list)
|
| 55 |
+
open_thinking: bool = False
|
| 56 |
+
chat_template_kwargs: dict = None
|
| 57 |
+
|
| 58 |
+
def get_open_thinking(self) -> bool:
|
| 59 |
+
"""兼容多种方式开启 thinking"""
|
| 60 |
+
if self.open_thinking:
|
| 61 |
+
return True
|
| 62 |
+
if self.chat_template_kwargs:
|
| 63 |
+
return self.chat_template_kwargs.get('open_thinking', False) or \
|
| 64 |
+
self.chat_template_kwargs.get('enable_thinking', False)
|
| 65 |
+
return False
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class CustomStreamer(TextStreamer):
|
| 69 |
+
def __init__(self, tokenizer, queue):
|
| 70 |
+
super().__init__(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 71 |
+
self.queue = queue
|
| 72 |
+
self.tokenizer = tokenizer
|
| 73 |
+
|
| 74 |
+
def on_finalized_text(self, text: str, stream_end: bool = False):
|
| 75 |
+
self.queue.put(text)
|
| 76 |
+
if stream_end:
|
| 77 |
+
self.queue.put(None)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def parse_response(text):
|
| 81 |
+
reasoning_content = None
|
| 82 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 83 |
+
if think_match:
|
| 84 |
+
reasoning_content = think_match.group(1).strip()
|
| 85 |
+
text = re.sub(r'<think>.*?</think>\s*', '', text, flags=re.DOTALL)
|
| 86 |
+
elif '</think>' in text:
|
| 87 |
+
parts = text.split('</think>', 1)
|
| 88 |
+
reasoning_content = parts[0].strip()
|
| 89 |
+
text = parts[1].strip() if len(parts) > 1 else ''
|
| 90 |
+
tool_calls = []
|
| 91 |
+
for i, m in enumerate(re.findall(r'<tool_call>(.*?)</tool_call>', text, re.DOTALL)):
|
| 92 |
+
try:
|
| 93 |
+
call = json.loads(m.strip())
|
| 94 |
+
tool_calls.append({"id": f"call_{int(time.time())}_{i}", "type": "function", "function": {"name": call.get("name", ""), "arguments": json.dumps(call.get("arguments", {}), ensure_ascii=False)}})
|
| 95 |
+
except Exception:
|
| 96 |
+
pass
|
| 97 |
+
if tool_calls:
|
| 98 |
+
text = re.sub(r'<tool_call>.*?</tool_call>', '', text, flags=re.DOTALL)
|
| 99 |
+
return text.strip(), reasoning_content, tool_calls or None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def generate_stream_response(messages, temperature, top_p, max_tokens, tools=None, open_thinking=False):
|
| 103 |
+
try:
|
| 104 |
+
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)
|
| 105 |
+
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
| 106 |
+
|
| 107 |
+
queue = Queue()
|
| 108 |
+
streamer = CustomStreamer(tokenizer, queue)
|
| 109 |
+
|
| 110 |
+
def _generate():
|
| 111 |
+
try:
|
| 112 |
+
model.generate(
|
| 113 |
+
inputs.input_ids,
|
| 114 |
+
max_new_tokens=max_tokens,
|
| 115 |
+
do_sample=True,
|
| 116 |
+
temperature=temperature,
|
| 117 |
+
top_p=top_p,
|
| 118 |
+
attention_mask=inputs.attention_mask,
|
| 119 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 120 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 121 |
+
streamer=streamer
|
| 122 |
+
)
|
| 123 |
+
except Exception as e:
|
| 124 |
+
queue.put({"error": str(e)})
|
| 125 |
+
queue.put(None)
|
| 126 |
+
|
| 127 |
+
Thread(target=_generate).start()
|
| 128 |
+
|
| 129 |
+
full_text = ""
|
| 130 |
+
emitted = 0
|
| 131 |
+
thinking_ended = not bool(open_thinking)
|
| 132 |
+
|
| 133 |
+
while True:
|
| 134 |
+
text = queue.get()
|
| 135 |
+
if text is None:
|
| 136 |
+
break
|
| 137 |
+
if isinstance(text, dict):
|
| 138 |
+
yield json.dumps(text, ensure_ascii=False)
|
| 139 |
+
continue
|
| 140 |
+
full_text += text
|
| 141 |
+
|
| 142 |
+
if not thinking_ended:
|
| 143 |
+
pos = full_text.find('</think>')
|
| 144 |
+
if pos >= 0:
|
| 145 |
+
thinking_ended = True
|
| 146 |
+
new_r = full_text[emitted:pos]
|
| 147 |
+
if new_r:
|
| 148 |
+
yield json.dumps({"choices": [{"delta": {"reasoning_content": new_r}}]}, ensure_ascii=False)
|
| 149 |
+
emitted = pos + len('</think>')
|
| 150 |
+
after = full_text[emitted:].lstrip('\n')
|
| 151 |
+
emitted = len(full_text) - len(after)
|
| 152 |
+
if after:
|
| 153 |
+
yield json.dumps({"choices": [{"delta": {"content": after}}]}, ensure_ascii=False)
|
| 154 |
+
emitted = len(full_text)
|
| 155 |
+
else:
|
| 156 |
+
new_r = full_text[emitted:]
|
| 157 |
+
if new_r:
|
| 158 |
+
yield json.dumps({"choices": [{"delta": {"reasoning_content": new_r}}]}, ensure_ascii=False)
|
| 159 |
+
emitted = len(full_text)
|
| 160 |
+
else:
|
| 161 |
+
new_c = full_text[emitted:]
|
| 162 |
+
if new_c:
|
| 163 |
+
yield json.dumps({"choices": [{"delta": {"content": new_c}}]}, ensure_ascii=False)
|
| 164 |
+
emitted = len(full_text)
|
| 165 |
+
|
| 166 |
+
_, _, tool_calls = parse_response(full_text)
|
| 167 |
+
if tool_calls:
|
| 168 |
+
yield json.dumps({"choices": [{"delta": {"tool_calls": tool_calls}}]}, ensure_ascii=False)
|
| 169 |
+
yield json.dumps({"choices": [{"delta": {}, "finish_reason": "tool_calls" if tool_calls else "stop"}]}, ensure_ascii=False)
|
| 170 |
+
|
| 171 |
+
except Exception as e:
|
| 172 |
+
yield json.dumps({"error": str(e)})
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@app.post("/v1/chat/completions")
|
| 176 |
+
async def chat_completions(request: ChatRequest):
|
| 177 |
+
try:
|
| 178 |
+
if request.stream:
|
| 179 |
+
return StreamingResponse(
|
| 180 |
+
(f"data: {chunk}\n\n" for chunk in generate_stream_response(
|
| 181 |
+
messages=request.messages,
|
| 182 |
+
temperature=request.temperature,
|
| 183 |
+
top_p=request.top_p,
|
| 184 |
+
max_tokens=request.max_tokens,
|
| 185 |
+
tools=request.tools,
|
| 186 |
+
open_thinking=request.get_open_thinking()
|
| 187 |
+
)),
|
| 188 |
+
media_type="text/event-stream"
|
| 189 |
+
)
|
| 190 |
+
else:
|
| 191 |
+
new_prompt = tokenizer.apply_chat_template(
|
| 192 |
+
request.messages,
|
| 193 |
+
tokenize=False,
|
| 194 |
+
add_generation_prompt=True,
|
| 195 |
+
tools=request.tools or None,
|
| 196 |
+
open_thinking=request.get_open_thinking()
|
| 197 |
+
)
|
| 198 |
+
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
| 199 |
+
with torch.no_grad():
|
| 200 |
+
generated_ids = model.generate(
|
| 201 |
+
inputs["input_ids"],
|
| 202 |
+
max_length=inputs["input_ids"].shape[1] + request.max_tokens,
|
| 203 |
+
do_sample=True,
|
| 204 |
+
attention_mask=inputs["attention_mask"],
|
| 205 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 206 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 207 |
+
top_p=request.top_p,
|
| 208 |
+
temperature=request.temperature
|
| 209 |
+
)
|
| 210 |
+
answer = tokenizer.decode(generated_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 211 |
+
content, reasoning_content, tool_calls = parse_response(answer)
|
| 212 |
+
message = {"role": "assistant", "content": content}
|
| 213 |
+
if reasoning_content:
|
| 214 |
+
message["reasoning_content"] = reasoning_content
|
| 215 |
+
if tool_calls:
|
| 216 |
+
message["tool_calls"] = tool_calls
|
| 217 |
+
return {
|
| 218 |
+
"id": f"chatcmpl-{int(time.time())}",
|
| 219 |
+
"object": "chat.completion",
|
| 220 |
+
"created": int(time.time()),
|
| 221 |
+
"model": "minimind",
|
| 222 |
+
"choices": [
|
| 223 |
+
{
|
| 224 |
+
"index": 0,
|
| 225 |
+
"message": message,
|
| 226 |
+
"finish_reason": "tool_calls" if tool_calls else "stop"
|
| 227 |
+
}
|
| 228 |
+
]
|
| 229 |
+
}
|
| 230 |
+
except Exception as e:
|
| 231 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
if __name__ == "__main__":
|
| 235 |
+
parser = argparse.ArgumentParser(description="Server for MiniMind")
|
| 236 |
+
parser.add_argument('--load_from', default='../model', type=str, help="模型加载路径(model=原生torch权重,其他路径=transformers格式)")
|
| 237 |
+
parser.add_argument('--save_dir', default='out', type=str, help="模型权重目录")
|
| 238 |
+
parser.add_argument('--weight', default='full_sft', type=str, help="权重名称前缀(pretrain, full_sft, dpo, reason, ppo_actor, grpo, spo)")
|
| 239 |
+
parser.add_argument('--lora_weight', default='None', type=str, help="LoRA权重名称(None表示不使用,可选:lora_identity, lora_medical)")
|
| 240 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 241 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 242 |
+
parser.add_argument('--max_seq_len', default=8192, type=int, help="最大序列长度")
|
| 243 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 244 |
+
parser.add_argument('--inference_rope_scaling', default=False, action='store_true', help="启用RoPE位置编码外推(4倍,仅解决位置编码问题)")
|
| 245 |
+
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', type=str, help="运行设备")
|
| 246 |
+
args = parser.parse_args()
|
| 247 |
+
device = args.device
|
| 248 |
+
model, tokenizer = init_model(args)
|
| 249 |
+
uvicorn.run(app, host="0.0.0.0", port=8998)
|
|
@@ -0,0 +1,748 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>MiniMind-O</title>
|
| 7 |
+
<link rel="icon" type="image/png" href="https://www.modelscope.cn/studio/gongjy/MiniMind/resolve/master/images/logo2.png">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg:#1e1e20; --p1:#2a2a2e; --p2:#35353b; --p3:#40404a;
|
| 11 |
+
--bd:#55555e; --bd2:#7a7a85;
|
| 12 |
+
--fg:#ececef; --mu:#9a9aa0; --dim:#8a8a92;
|
| 13 |
+
--u:#7fc5e0; --a:#e5b57a; --danger:#d84545; --cam:#7ad88a;
|
| 14 |
+
--serif:'Georgia','Times New Roman',serif; --mono:'SF Mono',Consolas,monospace;
|
| 15 |
+
}
|
| 16 |
+
*{margin:0;padding:0;box-sizing:border-box}
|
| 17 |
+
html,body{height:100%}
|
| 18 |
+
body{height:100vh;background:var(--bg);color:var(--fg);font-family:var(--serif);display:flex;align-items:center;justify-content:center;overflow:hidden}
|
| 19 |
+
.app{max-width:860px;width:100%;height:calc(100vh - 40px);display:flex;flex-direction:column;padding:20px 24px;background:var(--p1);border:1px solid var(--bd);overflow:hidden}
|
| 20 |
+
.topbar{display:flex;align-items:center;justify-content:space-between;padding:4px 0 12px;flex-shrink:0;gap:10px}
|
| 21 |
+
.brand{display:flex;align-items:center;gap:10px;min-width:0;flex:1}
|
| 22 |
+
.brand>div{min-width:0}
|
| 23 |
+
.brand img{height:34px}
|
| 24 |
+
.brand h1{font-size:1.15rem;font-weight:700;font-style:italic;color:#fff}
|
| 25 |
+
.brand .sub{font-size:.72rem;color:var(--mu);font-style:italic}
|
| 26 |
+
.topright{display:flex;gap:8px;align-items:center}
|
| 27 |
+
.btn{padding:5px 10px;font-size:.75rem;cursor:pointer;background:var(--p2);border:1px solid var(--bd);color:var(--fg);font-family:var(--serif);font-style:italic;display:inline-flex;align-items:center;gap:5px}
|
| 28 |
+
.btn:hover:not(:disabled){background:var(--p3);border-color:var(--bd2)}
|
| 29 |
+
.btn:disabled{opacity:.4;cursor:not-allowed}
|
| 30 |
+
.btn.danger{color:var(--danger);border-color:#6a3a3a}
|
| 31 |
+
.btn.danger:hover:not(:disabled){background:var(--danger);border-color:var(--danger);color:#fff}
|
| 32 |
+
.btn svg{width:12px;height:12px;fill:currentColor}
|
| 33 |
+
.model-sel{max-width:150px;padding:5px 8px;font-size:.72rem;background:var(--p2);border:1px solid var(--bd);color:var(--fg);font-family:var(--serif);font-style:italic;outline:none}
|
| 34 |
+
.brand .model-sel{max-width:260px;padding:0 34px 0 0;background:transparent;border:none;font-size:1.15rem;font-weight:700;color:#fff}
|
| 35 |
+
.model-sel:focus{border-color:var(--bd2)}
|
| 36 |
+
.mini-sel{max-width:130px;padding:5px 8px;font-size:.72rem;background:var(--p2);border:1px solid var(--bd);color:var(--fg);font-family:var(--serif);font-style:italic;outline:none}
|
| 37 |
+
.mini-sel:focus{border-color:var(--bd2)}
|
| 38 |
+
.voice-sel{display:none}
|
| 39 |
+
.more{position:relative}
|
| 40 |
+
.more-pop{top:100%;right:0;left:auto;margin-top:6px;min-width:130px}
|
| 41 |
+
.vgroup{padding:4px 10px;font-size:.6rem;color:var(--mu);font-style:italic}
|
| 42 |
+
.vitem{padding:5px 10px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;font-size:.72rem;font-family:var(--serif);font-style:italic;color:var(--fg);gap:8px}
|
| 43 |
+
.vitem:hover{background:var(--p3)}
|
| 44 |
+
.vitem.on{color:var(--u)}
|
| 45 |
+
.vx{color:var(--mu);cursor:pointer;font-style:normal;font-size:.8rem;padding:0 2px;line-height:1}
|
| 46 |
+
.vx:hover{color:var(--danger)}
|
| 47 |
+
.vadd{padding:5px 10px;font-size:.72rem;color:var(--mu);cursor:pointer;font-family:var(--serif);font-style:italic;border-top:1px solid var(--bd)}
|
| 48 |
+
.vadd:hover{background:var(--p3);color:var(--fg)}
|
| 49 |
+
.status{display:inline-flex;align-items:center;gap:10px;padding:5px 10px;font-size:.65rem;font-family:var(--mono);background:var(--p2);border:1px solid var(--bd);transition:opacity .3s}
|
| 50 |
+
.status.hidden{opacity:0;pointer-events:none}
|
| 51 |
+
.status .row{display:inline-flex;gap:5px;white-space:nowrap}
|
| 52 |
+
.status .lab{color:var(--mu);font-style:italic}
|
| 53 |
+
|
| 54 |
+
.chat{flex:1;overflow-y:auto;padding:12px 0;min-height:0;display:flex;flex-direction:column;gap:18px;scrollbar-width:none;-ms-overflow-style:none}
|
| 55 |
+
.chat::-webkit-scrollbar{display:none}
|
| 56 |
+
.msg{display:flex;flex-direction:column;gap:6px;animation:fi .25s ease}
|
| 57 |
+
.msg.user{align-items:flex-end}
|
| 58 |
+
.msg.user .body{align-items:flex-end}
|
| 59 |
+
@keyframes fi{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}
|
| 60 |
+
.meta{font-size:.72rem;font-style:italic}
|
| 61 |
+
.msg.user .meta{color:var(--u)}
|
| 62 |
+
.msg.assistant .meta{color:var(--a)}
|
| 63 |
+
.body{display:flex;flex-direction:column;gap:8px}
|
| 64 |
+
.text{line-height:1.65;white-space:pre-wrap;word-break:break-word}
|
| 65 |
+
.img{display:block;max-width:160px;max-height:160px}
|
| 66 |
+
.msg.user .text:not(.voice){color:var(--u)}
|
| 67 |
+
.voice{font-family:var(--mono);font-size:1.05rem;line-height:1.2;color:var(--bd)}
|
| 68 |
+
.msg.user .voice{color:rgba(127,197,224,.5)}
|
| 69 |
+
.prog{text-decoration:underline dashed;text-decoration-color:var(--bd);text-underline-offset:4px;text-decoration-thickness:1px}
|
| 70 |
+
.prog .tk{cursor:pointer;padding:1px 0;transition:color .08s,background .08s}
|
| 71 |
+
.msg.assistant .prog .tk.played{color:var(--a)}
|
| 72 |
+
.msg.assistant .prog .tk:hover{background:rgba(229,181,122,.2)}
|
| 73 |
+
.msg.assistant .prog.on .tk.cur{background:rgba(229,181,122,.45)}
|
| 74 |
+
.msg.user .prog .tk.played{color:var(--u)}
|
| 75 |
+
.msg.user .prog .tk:hover{background:rgba(127,197,224,.18)}
|
| 76 |
+
.msg.user .prog.on .tk.cur{background:rgba(127,197,224,.35)}
|
| 77 |
+
.player{display:inline-flex;align-items:center;gap:10px;padding:6px 12px;width:320px;max-width:100%;background:var(--p2);border:1px solid var(--bd)}
|
| 78 |
+
.pp{background:transparent;border:none;color:inherit;cursor:pointer;font-family:var(--mono);font-size:.9rem;width:20px;text-align:left}
|
| 79 |
+
.pp:hover{opacity:.75}
|
| 80 |
+
.bar-wrap{flex:1;height:3px;cursor:pointer;background:var(--p1)}
|
| 81 |
+
.bar{height:100%;width:0;transition:width .08s linear}
|
| 82 |
+
.msg.user .bar,.msg.user .pp{background:var(--u);color:var(--u)}
|
| 83 |
+
.msg.assistant .bar,.msg.assistant .pp{background:var(--a);color:var(--a)}
|
| 84 |
+
.msg.user .bar-wrap{background:var(--p1)}
|
| 85 |
+
.time{font-size:.7rem;font-family:var(--mono);min-width:76px;text-align:right;color:var(--mu)}
|
| 86 |
+
|
| 87 |
+
.input-area{flex-shrink:0;padding:10px 0 4px}
|
| 88 |
+
.compose{display:flex;gap:6px}
|
| 89 |
+
.compose button{padding:0 12px;min-width:42px;cursor:pointer;font-size:.85rem;background:var(--p2);border:1px solid var(--bd);color:var(--fg);display:flex;align-items:center;justify-content:center;font-family:inherit}
|
| 90 |
+
.compose button svg{width:16px;height:16px;fill:currentColor}
|
| 91 |
+
.compose button:hover:not(:disabled){background:var(--p3);border-color:var(--bd2)}
|
| 92 |
+
.compose button:disabled{opacity:.4;cursor:not-allowed}
|
| 93 |
+
.compose button.primary{background:var(--fg);color:#1a1a1e;border-color:var(--fg)}
|
| 94 |
+
.compose button.primary:hover:not(:disabled){background:#fff}
|
| 95 |
+
.compose button.rec{animation:pulse 1s infinite;background:var(--danger);color:#fff;border-color:var(--danger)}
|
| 96 |
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
| 97 |
+
.text-wrap{flex:1;display:flex;align-items:center;gap:6px;padding:0 6px 0 10px;min-width:0;background:var(--p2);border:1px solid var(--bd)}
|
| 98 |
+
.text-wrap:focus-within{border-color:var(--fg)}
|
| 99 |
+
.text-wrap input{flex:1;min-width:0;padding:10px 4px;font-size:.9rem;outline:none;background:transparent;border:none;color:inherit;font-family:inherit}
|
| 100 |
+
.text-wrap input::placeholder{color:var(--dim)}
|
| 101 |
+
.attach{display:flex;gap:4px;flex-wrap:nowrap;flex-shrink:0;max-width:45%;overflow-x:auto;scrollbar-width:none}
|
| 102 |
+
.attach::-webkit-scrollbar{display:none}
|
| 103 |
+
.attach:empty{display:none}
|
| 104 |
+
.chip{display:inline-flex;align-items:center;gap:6px;padding:3px 7px;font-size:.72rem;background:var(--p1);border:1px solid var(--bd);flex-shrink:0}
|
| 105 |
+
.chip .x{cursor:pointer;opacity:.55;font-size:.85rem}
|
| 106 |
+
.chip .x:hover{opacity:1}
|
| 107 |
+
.chip .play{cursor:pointer}
|
| 108 |
+
.chip img{width:22px;height:22px;object-fit:cover}
|
| 109 |
+
.pop{position:absolute;background:var(--p2);border:1px solid var(--bd);display:none;flex-direction:column;z-index:50;min-width:110px;font-family:var(--serif);font-style:italic;white-space:nowrap}
|
| 110 |
+
.pop.on{display:flex}
|
| 111 |
+
.pop button{padding:5px 10px;background:transparent;border:none;color:var(--fg);cursor:pointer;text-align:left;font-size:.72rem;font-family:inherit;font-style:inherit;line-height:1.2}
|
| 112 |
+
.pop button:hover{background:var(--p3)}
|
| 113 |
+
.snap{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;flex-direction:column;gap:14px}
|
| 114 |
+
.snap.on{display:flex}
|
| 115 |
+
.snap video{max-width:80vw;max-height:70vh;background:#000;border:1px solid var(--bd)}
|
| 116 |
+
.snap .sr{display:flex;gap:10px}
|
| 117 |
+
.snap .sr button{padding:8px 18px;background:var(--p2);border:1px solid var(--bd);color:var(--fg);cursor:pointer;font-family:var(--serif);font-style:italic}
|
| 118 |
+
.snap .sr button.primary{background:var(--fg);color:#1a1a1e;border-color:var(--fg)}
|
| 119 |
+
.clone-card{width:min(92vw,520px);padding:22px;background:var(--p1);border:1px solid var(--bd);display:flex;flex-direction:column;gap:14px}
|
| 120 |
+
.clone-title{font-size:1rem;font-style:italic;color:#fff}
|
| 121 |
+
.clone-hint{font-size:.76rem;color:var(--mu);font-style:italic;line-height:1.6}
|
| 122 |
+
.clone-name{width:100%;padding:10px 12px;background:var(--p2);border:1px solid var(--bd);color:var(--fg);font-family:inherit;font-size:.85rem;outline:none}
|
| 123 |
+
.clone-name:focus{border-color:var(--fg)}
|
| 124 |
+
.clone-script{padding:12px 14px;background:var(--p2);border:1px solid var(--bd);font-size:1rem;line-height:1.85}
|
| 125 |
+
.clone-stat{min-height:18px;font-size:.72rem;color:var(--mu);font-family:var(--mono)}
|
| 126 |
+
|
| 127 |
+
/* ===== Call mode ===== */
|
| 128 |
+
body>.app,#call{will-change:transform,opacity,filter;transform-origin:center center}
|
| 129 |
+
body>.app{transition:opacity .55s cubic-bezier(.22,1,.36,1),transform .7s cubic-bezier(.22,1,.36,1),filter .55s cubic-bezier(.22,1,.36,1)}
|
| 130 |
+
body.call>.app{opacity:0;transform:scale(.92);filter:blur(8px);pointer-events:none}
|
| 131 |
+
#call{position:fixed;inset:0;z-index:100;background:var(--bg);overflow:hidden;opacity:0;transform:scale(1.08);filter:blur(10px);pointer-events:none;visibility:hidden;transition:opacity .55s cubic-bezier(.22,1,.36,1),transform .7s cubic-bezier(.22,1,.36,1),filter .55s cubic-bezier(.22,1,.36,1),visibility 0s .7s}
|
| 132 |
+
body.call #call{opacity:1;transform:scale(1);filter:blur(0);pointer-events:auto;visibility:visible;transition:opacity .55s cubic-bezier(.22,1,.36,1),transform .7s cubic-bezier(.22,1,.36,1),filter .55s cubic-bezier(.22,1,.36,1),visibility 0s 0s}
|
| 133 |
+
#call .app{max-width:560px;margin:20px auto;padding:18px 22px;position:relative}
|
| 134 |
+
#call .topbar{border-bottom:1px dashed var(--bd)}
|
| 135 |
+
#call .brand h1{font-size:1.05rem}
|
| 136 |
+
#call .brand .model-sel{max-width:230px;padding-right:38px;font-size:1.05rem}
|
| 137 |
+
#call .brand .sub{font-size:.68rem}
|
| 138 |
+
.pill{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;font-size:.7rem;font-family:var(--mono);background:var(--p2);border:1px solid var(--bd);color:var(--mu);font-style:italic;line-height:1.2}
|
| 139 |
+
.pill .dot{width:6px;height:6px;background:var(--mu);border-radius:50%}
|
| 140 |
+
.pill.live{color:var(--u);border-color:#3c5966}
|
| 141 |
+
.pill.live .dot{background:var(--u);box-shadow:0 0 6px var(--u);animation:cp 1.4s ease-in-out infinite}
|
| 142 |
+
@keyframes cp{0%,100%{opacity:1}50%{opacity:.4}}
|
| 143 |
+
#call .btn.active{background:var(--u);color:#1a1a1e;border-color:var(--u)}
|
| 144 |
+
|
| 145 |
+
.hero{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;padding:14px 0;position:relative}
|
| 146 |
+
.orb{position:relative;width:280px;height:280px;max-width:60vh;max-height:60vh;display:flex;align-items:center;justify-content:center;will-change:transform}
|
| 147 |
+
.orb svg{width:100%;height:100%;overflow:visible}
|
| 148 |
+
.blob{fill:none;stroke-linejoin:round;transition:stroke .35s,opacity .45s}
|
| 149 |
+
.blob.l0{stroke:#5a5a64;stroke-width:1.2;opacity:.55}
|
| 150 |
+
.blob.l1{stroke:var(--bd2);stroke-width:1.4;opacity:.85}
|
| 151 |
+
.blob.l2{stroke:var(--fg);stroke-width:1.6}
|
| 152 |
+
.orb.camon .blob.l0,.orb.camon .blob.l1{opacity:0}
|
| 153 |
+
.orb.camon .blob.l2{stroke:var(--cam);filter:drop-shadow(0 0 4px rgba(122,216,138,.45))}
|
| 154 |
+
svg.listen .blob.l2{stroke:var(--u);filter:drop-shadow(0 0 4px rgba(127,197,224,.45))}
|
| 155 |
+
svg.listen .blob.l1{stroke:rgba(127,197,224,.6)}
|
| 156 |
+
svg.speak .blob.l2{stroke:var(--a);filter:drop-shadow(0 0 5px rgba(229,181,122,.5))}
|
| 157 |
+
svg.speak .blob.l1{stroke:rgba(229,181,122,.6)}
|
| 158 |
+
svg.gen .blob.l2{stroke:var(--fg);filter:drop-shadow(0 0 4px rgba(236,236,239,.35))}
|
| 159 |
+
.core{display:none}
|
| 160 |
+
svg.listen .core{fill:var(--u)}
|
| 161 |
+
svg.speak .core{fill:var(--a)}
|
| 162 |
+
.ripple{fill:none;stroke:var(--mu);stroke-width:1;transform-origin:center;animation:rip 1.6s ease-out forwards}
|
| 163 |
+
.ripple.user{stroke:var(--u)}
|
| 164 |
+
.ripple.assistant{stroke:var(--a)}
|
| 165 |
+
@keyframes rip{0%{transform:scale(.55);opacity:.55}100%{transform:scale(1.35);opacity:0}}
|
| 166 |
+
.lbl{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:6px}
|
| 167 |
+
.ic{width:34px;height:34px;color:var(--mu);transition:color .3s;display:flex;align-items:center;justify-content:center}
|
| 168 |
+
.ic svg{width:100%;height:100%;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
| 169 |
+
.lbl.listen .ic{color:var(--u)}
|
| 170 |
+
.lbl.speak .ic{color:var(--a)}
|
| 171 |
+
.lbl.gen .ic{color:var(--fg)}
|
| 172 |
+
.lbl .st{display:none}
|
| 173 |
+
.ic svg *{transform-box:view-box;transform-origin:12px 12px}
|
| 174 |
+
.lbl.idle .ic .dot{animation:breathe 2.2s ease-in-out infinite;transform-origin:center;transform-box:view-box}
|
| 175 |
+
@keyframes breathe{0%,100%{opacity:.4;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}
|
| 176 |
+
.lbl.listen .ic .w{animation:sigin 1.6s ease-out infinite}
|
| 177 |
+
.lbl.listen .ic .w2{animation-delay:.3s}
|
| 178 |
+
.lbl.listen .ic .w3{animation-delay:.6s}
|
| 179 |
+
@keyframes sigin{0%{opacity:0;transform:scale(1.2)}60%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.85)}}
|
| 180 |
+
.lbl.gen .ic svg{animation:spin 1.2s cubic-bezier(.5,.1,.5,.9) infinite}
|
| 181 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 182 |
+
.lbl.speak .ic .a1{animation:radout 1.3s ease-out infinite}
|
| 183 |
+
.lbl.speak .ic .a2{animation:radout 1.3s ease-out .25s infinite}
|
| 184 |
+
.lbl.speak .ic .a3{animation:radout 1.3s ease-out .5s infinite}
|
| 185 |
+
@keyframes radout{0%{opacity:0;transform:scale(.7)}40%{opacity:1}100%{opacity:0;transform:scale(1.15)}}
|
| 186 |
+
.up{display:none}
|
| 187 |
+
.cam{position:absolute;top:50%;left:50%;width:180px;height:180px;object-fit:cover;background:var(--bg);border:1px solid var(--bd);transform:translate(-50%,-50%) scale(.3);opacity:0;transition:transform .5s cubic-bezier(.4,0,.2,1),opacity .3s,border-color .35s,box-shadow .35s;pointer-events:none;will-change:transform,opacity}
|
| 188 |
+
.orb.camon .cam{transform:translate(-50%,-50%) scale(1);opacity:1;border-color:var(--cam);box-shadow:0 0 14px rgba(122,216,138,.35)}
|
| 189 |
+
.orb.camon .core{opacity:0}
|
| 190 |
+
.orb.camon .lbl{top:auto;bottom:-8px;transform:translate(-50%,100%)}
|
| 191 |
+
.orb.camon .lbl .st{font-size:.9rem}
|
| 192 |
+
svg.listen ~ .cam{border-color:var(--u);box-shadow:0 0 14px rgba(127,197,224,.45)}
|
| 193 |
+
svg.speak ~ .cam{border-color:var(--a);box-shadow:0 0 14px rgba(229,181,122,.45)}
|
| 194 |
+
|
| 195 |
+
.latest{width:100%;padding:10px 14px;background:var(--p2);border:1px solid var(--bd);display:flex;flex-direction:column;gap:5px;flex-shrink:0;max-height:120px;overflow-y:auto;scrollbar-width:none}
|
| 196 |
+
.latest::-webkit-scrollbar{display:none}
|
| 197 |
+
.latest .lm{font-family:var(--mono);font-size:.6rem;color:var(--mu);font-style:italic}
|
| 198 |
+
.latest .lt{font-size:.82rem;line-height:1.55;font-style:italic;opacity:.9;white-space:pre-wrap;word-break:break-word}
|
| 199 |
+
.latest.user .lm{color:var(--u)}
|
| 200 |
+
.latest.assistant .lm{color:var(--a)}
|
| 201 |
+
.latest.empty .lt{color:var(--bd)}
|
| 202 |
+
.ctrls{display:flex;justify-content:center;gap:10px;padding:14px 0 2px;margin-top:12px;border-top:1px dashed var(--bd);flex-shrink:0}
|
| 203 |
+
.cbtn{width:72px;height:58px;background:var(--p2);border:1px solid var(--bd);color:var(--fg);cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;font-family:var(--mono);font-size:.58rem;letter-spacing:1px;text-transform:uppercase;transition:all .18s}
|
| 204 |
+
.cbtn svg{width:18px;height:18px;fill:currentColor}
|
| 205 |
+
.cbtn:hover{background:var(--p3);border-color:var(--bd2)}
|
| 206 |
+
.cbtn.active{background:var(--u);color:#1a1a1e;border-color:var(--u)}
|
| 207 |
+
.cbtn.danger{color:var(--danger);border-color:#6a3a3a}
|
| 208 |
+
.cbtn.danger:hover{background:var(--danger);color:#fff}
|
| 209 |
+
|
| 210 |
+
.trans{position:absolute;inset:0;background:var(--p1);z-index:20;display:flex;flex-direction:column;padding:18px 22px;transform:translateY(100%);transition:transform .3s ease;will-change:transform;contain:layout paint}
|
| 211 |
+
.trans.on{transform:translateY(0)}
|
| 212 |
+
.trans .th{display:flex;justify-content:space-between;padding-bottom:10px;border-bottom:1px dashed var(--bd)}
|
| 213 |
+
.trans .tt{font-style:italic;font-size:.95rem;display:flex;gap:10px;align-items:center}
|
| 214 |
+
.trans .cnt{font-family:var(--mono);font-size:.68rem;color:var(--mu)}
|
| 215 |
+
#call .chat:empty::before{content:'no turns yet · say something';display:block;text-align:center;color:var(--bd);font-style:italic;padding:40px 0;font-size:.82rem}
|
| 216 |
+
@media(max-width:540px){
|
| 217 |
+
body{display:block}
|
| 218 |
+
.app{height:100vh;height:100dvh;max-width:none;border:none;padding:14px 16px}
|
| 219 |
+
.brand img{height:28px}
|
| 220 |
+
.brand h1{font-size:1rem}
|
| 221 |
+
.brand .sub{display:none}
|
| 222 |
+
.topbar{gap:6px}
|
| 223 |
+
.status{display:none!important}
|
| 224 |
+
.player{width:100%}
|
| 225 |
+
.vitem,.vadd{padding:8px 12px}
|
| 226 |
+
.compose button{min-width:38px;padding:0 10px}
|
| 227 |
+
.text-wrap input{padding:8px 4px;font-size:.85rem}
|
| 228 |
+
.clone-card{padding:16px}
|
| 229 |
+
.clone-hint{font-size:.7rem}
|
| 230 |
+
#call .app{margin:0;height:100vh;height:100dvh;border:none;padding:14px 16px}
|
| 231 |
+
.orb{width:240px;height:240px}
|
| 232 |
+
.cbtn{width:58px;height:52px}
|
| 233 |
+
}
|
| 234 |
+
</style>
|
| 235 |
+
</head>
|
| 236 |
+
<body>
|
| 237 |
+
<div class="app">
|
| 238 |
+
<div class="topbar">
|
| 239 |
+
<div class="brand">
|
| 240 |
+
<img src="https://www.modelscope.cn/studio/gongjy/MiniMind/resolve/master/images/logo2.png" alt="logo">
|
| 241 |
+
<div><select class="model-sel" id="modelSel"></select><div class="sub">multimodal inference · text / image / audio → text + audio</div></div>
|
| 242 |
+
</div>
|
| 243 |
+
<div class="topright">
|
| 244 |
+
<div class="status hidden" id="stat"><div class="row"><span class="lab">text_ttft</span><span id="tt">—</span></div><div class="row"><span class="lab">audio_ttft</span><span id="ta">—</span></div><div class="row"><span class="lab">status</span><span id="ts">idle</span></div></div>
|
| 245 |
+
<select class="voice-sel" id="voiceSel"><option value="default">default</option></select>
|
| 246 |
+
<div class="more">
|
| 247 |
+
<button class="btn" id="moreBtn">default</button>
|
| 248 |
+
<div class="pop more-pop" id="morePop"></div>
|
| 249 |
+
</div>
|
| 250 |
+
<button class="btn" id="callBtn"><svg viewBox="0 0 24 24"><path d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.32.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57.1.35 0 .74-.25 1.02l-2.2 2.2z"/></svg>Call</button>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
<div class="chat" id="chat"></div>
|
| 254 |
+
<div class="input-area"><div class="compose">
|
| 255 |
+
<div style="position:relative;display:flex">
|
| 256 |
+
<button id="imgBtn" title="image"><svg viewBox="0 0 24 24"><path d="M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zm-2 0H5l3.5-4.5 2.5 3.01L14.5 13l4.5 6zM8.5 8A1.5 1.5 0 1 1 7 9.5 1.5 1.5 0 0 1 8.5 8z"/></svg></button>
|
| 257 |
+
<div class="pop" id="imgPop" style="bottom:100%;left:0;margin-bottom:6px"><button id="fileBtn">from file</button><button id="shotBtn">take photo</button></div>
|
| 258 |
+
</div>
|
| 259 |
+
<input type="file" id="imgIn" accept="image/*" hidden>
|
| 260 |
+
<button id="recBtn" title="录音"><svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.91-3c-.49 0-.9.36-.98.85C16.52 14.2 14.47 16 12 16s-4.52-1.8-4.93-4.15c-.08-.49-.49-.85-.98-.85-.61 0-1.09.54-1 1.14.49 3 2.89 5.35 5.91 5.78V20c0 .55.45 1 1 1s1-.45 1-1v-2.08c3.02-.43 5.42-2.78 5.91-5.78.1-.6-.39-1.14-1-1.14z"/></svg></button>
|
| 261 |
+
<div class="text-wrap"><div class="attach" id="att"></div><input type="text" id="txt" placeholder="Message MiniMind-O..." autocomplete="off"></div>
|
| 262 |
+
<button id="sendBtn" class="primary"><svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button>
|
| 263 |
+
</div></div>
|
| 264 |
+
</div>
|
| 265 |
+
|
| 266 |
+
<div class="snap" id="snap"><video id="snapV" autoplay playsinline muted></video><div class="sr"><button id="snapCancel">cancel</button><button id="snapOk" class="primary">capture</button></div></div>
|
| 267 |
+
<div class="snap" id="cloneModal"><div class="clone-card"><div class="clone-title">Clone Voice</div><div class="clone-hint">请自然朗读下面这句话。建议 3 到 6 秒,环境尽量安静。录完后会自动做 1.5 倍速、提取 `CAM++` speaker embedding 与 `Mimi` ref codes,然后立即加入音色列表。</div><input class="clone-name" id="cloneName" placeholder="音色名,例如:小王" maxlength="24"><div class="clone-script" id="cloneText">今天阳光很好,欢迎来到我的语音克隆小实验。</div><div class="clone-stat" id="cloneStat">点击 start 开始录音</div><div class="sr"><button id="cloneCancel">cancel</button><button id="cloneRec" class="primary">start</button></div></div></div>
|
| 268 |
+
|
| 269 |
+
<div id="call"><div class="app">
|
| 270 |
+
<div class="topbar">
|
| 271 |
+
<div class="brand"><img src="https://www.modelscope.cn/studio/gongjy/MiniMind/resolve/master/images/logo2.png" alt=""><div><select class="model-sel" id="callModelSel"></select><div class="sub">live voice · bidirectional streaming</div></div></div>
|
| 272 |
+
<div class="topright">
|
| 273 |
+
<select class="mini-sel" id="callVoiceSel"><option value="default">default</option></select>
|
| 274 |
+
<button class="btn" id="logBtn"><svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6zm2-3h8v-2H8v2zm0-4h8v-2H8v2z"/></svg>log</button>
|
| 275 |
+
<span class="pill" id="pill"><span class="dot"></span><span id="connT">connecting</span></span>
|
| 276 |
+
</div>
|
| 277 |
+
</div>
|
| 278 |
+
<div class="hero"><div class="orb" id="orb">
|
| 279 |
+
<svg viewBox="-160 -160 320 320" id="osvg"><g id="rip"></g><path class="blob l0" id="b0"/><path class="blob l1" id="b1"/><path class="blob l2" id="b2"/><circle class="core" id="core" cx="0" cy="0" r="4"/></svg>
|
| 280 |
+
<video class="cam" id="camV" autoplay playsinline muted></video>
|
| 281 |
+
<div class="lbl" id="lbl"><div class="ic" id="stIc"></div><div class="st" id="stT">init</div><div class="up" id="upT">00:00:00</div></div>
|
| 282 |
+
</div></div>
|
| 283 |
+
<div class="latest empty" id="lat"><span class="lm" id="lmE">latest · awaiting first turn</span><span class="lt" id="ltE">speak naturally — model responds with voice and text</span></div>
|
| 284 |
+
<div class="ctrls">
|
| 285 |
+
<button class="cbtn" id="camBtn"><svg viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg><span>cam</span></button>
|
| 286 |
+
<button class="cbtn danger" id="endBtn"><svg viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg><span>end</span></button>
|
| 287 |
+
</div>
|
| 288 |
+
<div class="trans" id="trans"><div class="th"><div class="tt">session transcript <span class="cnt" id="tcnt">0 turns</span></div><button class="btn" id="closeBtn">× close</button></div><div class="chat" id="cchat"></div></div>
|
| 289 |
+
</div></div>
|
| 290 |
+
|
| 291 |
+
<script>
|
| 292 |
+
const $=id=>document.getElementById(id);
|
| 293 |
+
const chat=$('chat'),txt=$('txt'),sendBtn=$('sendBtn'),recBtn=$('recBtn'),imgBtn=$('imgBtn'),imgIn=$('imgIn'),callBtn=$('callBtn'),att=$('att');
|
| 294 |
+
const stat=$('stat'),tt=$('tt'),ta=$('ta'),ts=$('ts'),voiceSel=$('voiceSel'),modelSel=$('modelSel'),callModelSel=$('callModelSel'),callVoiceSel=$('callVoiceSel');
|
| 295 |
+
const moreBtn=$('moreBtn'),morePop=$('morePop');
|
| 296 |
+
const CLONE_PROMPT='今天阳光很好,欢迎来到我的语音克隆小实验。';
|
| 297 |
+
let voiceGroups={builtin:[],unseen:[],manual:[]};
|
| 298 |
+
const closeMore=()=>morePop.classList.remove('on');
|
| 299 |
+
const modelLabel=m=>m;
|
| 300 |
+
async function loadVoices(sel){
|
| 301 |
+
const keep=sel||voiceSel.value||'default';
|
| 302 |
+
const keepCall=callVoiceSel.value||'default';
|
| 303 |
+
voiceSel.innerHTML='<option value="default">default</option>';
|
| 304 |
+
const d=await fetch('/voices').then(r=>r.json());
|
| 305 |
+
voiceGroups={builtin:d.builtin||[],unseen:d.unseen||[],manual:d.manual||[]};
|
| 306 |
+
for(const [label,names] of [['builtin',voiceGroups.builtin],['unseen',voiceGroups.unseen],['manual',voiceGroups.manual]]){
|
| 307 |
+
if(!names.length)continue;
|
| 308 |
+
const g=document.createElement('optgroup');
|
| 309 |
+
g.label=label;
|
| 310 |
+
names.forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v;g.appendChild(o)});
|
| 311 |
+
voiceSel.appendChild(g);
|
| 312 |
+
}
|
| 313 |
+
if([...voiceSel.options].some(o=>o.value===keep))voiceSel.value=keep;
|
| 314 |
+
callVoiceSel.innerHTML=voiceSel.innerHTML;
|
| 315 |
+
if([...callVoiceSel.options].some(o=>o.value===keepCall))callVoiceSel.value=keepCall;
|
| 316 |
+
renderVoiceList();
|
| 317 |
+
}
|
| 318 |
+
function renderVoiceList(){
|
| 319 |
+
let h='';const sel=voiceSel.value;
|
| 320 |
+
moreBtn.textContent=sel;
|
| 321 |
+
h+=`<div class="vitem${sel==='default'?' on':''}" data-voice="default">default</div>`;
|
| 322 |
+
for(const [g,ns] of [['builtin',voiceGroups.builtin],['unseen',voiceGroups.unseen],['manual',voiceGroups.manual]]){
|
| 323 |
+
if(!ns.length)continue;
|
| 324 |
+
h+=`<div class="vgroup">${g}</div>`;
|
| 325 |
+
for(const v of ns)h+=`<div class="vitem${v===sel?' on':''}" data-voice="${v}"><span>${v}</span>${g==='manual'?`<span class="vx" data-del="${v}">×</span>`:''}</div>`;
|
| 326 |
+
}
|
| 327 |
+
h+='<div class="vadd">+ clone</div>';
|
| 328 |
+
morePop.innerHTML=h;
|
| 329 |
+
}
|
| 330 |
+
loadVoices().catch(()=>{});
|
| 331 |
+
async function loadModels(){
|
| 332 |
+
const d=await fetch('/models').then(r=>r.json());
|
| 333 |
+
modelSel.innerHTML='';callModelSel.innerHTML='';
|
| 334 |
+
for(const m of d.models||[]){const o=document.createElement('option');o.value=m;o.textContent=modelLabel(m);modelSel.appendChild(o);callModelSel.appendChild(o.cloneNode(true))}
|
| 335 |
+
if(d.current){modelSel.value=d.current;callModelSel.value=d.current}
|
| 336 |
+
}
|
| 337 |
+
async function switchModelSelect(sel, oldSel){
|
| 338 |
+
const old=sel.dataset.current||sel.value,name=sel.value;
|
| 339 |
+
const wasCall=document.body.classList.contains('call');
|
| 340 |
+
stopChat();if(callStop)callStop();stat.classList.remove('hidden');ts.textContent='switching';sel.disabled=true;
|
| 341 |
+
try{
|
| 342 |
+
const r=await fetch('/switch_model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});
|
| 343 |
+
const d=await r.json();
|
| 344 |
+
if(!r.ok||!d.ok)throw new Error(d.error||'switch failed');
|
| 345 |
+
modelSel.value=name;callModelSel.value=name;modelSel.dataset.current=name;callModelSel.dataset.current=name;ts.textContent=`loaded ${name}`;if(wasCall)openCall();
|
| 346 |
+
}catch(e){sel.value=old;if(oldSel)oldSel.value=old;ts.textContent='switch failed';alert(e.message)}
|
| 347 |
+
sel.disabled=false;
|
| 348 |
+
}
|
| 349 |
+
modelSel.onchange=()=>switchModelSelect(modelSel,callModelSel);
|
| 350 |
+
callModelSel.onchange=()=>switchModelSelect(callModelSel,modelSel);
|
| 351 |
+
callVoiceSel.onchange=()=>{if(window.callSendCtx)window.callSendCtx()};
|
| 352 |
+
loadModels().then(()=>{modelSel.dataset.current=modelSel.value;callModelSel.dataset.current=callModelSel.value}).catch(()=>{});
|
| 353 |
+
|
| 354 |
+
/* ------------- shared helpers ------------- */
|
| 355 |
+
function decPcm(b64){
|
| 356 |
+
const bin=atob(b64),i16=new Int16Array(bin.length/2);
|
| 357 |
+
for(let i=0;i<i16.length;i++)i16[i]=bin.charCodeAt(i*2)|(bin.charCodeAt(i*2+1)<<8);
|
| 358 |
+
const f32=new Float32Array(i16.length),fade=240;
|
| 359 |
+
for(let i=0;i<i16.length;i++)f32[i]=i16[i]/32768;
|
| 360 |
+
for(let i=0;i<fade&&i<f32.length;i++){f32[i]*=i/fade;f32[f32.length-1-i]*=i/fade;}
|
| 361 |
+
return {i16,f32};
|
| 362 |
+
}
|
| 363 |
+
function buildWav(chunks,sr){
|
| 364 |
+
let n=0;for(const c of chunks)n+=c.length;
|
| 365 |
+
const buf=new ArrayBuffer(44+n*2),v=new DataView(buf),w=(o,s)=>{for(let i=0;i<s.length;i++)v.setUint8(o+i,s.charCodeAt(i))};
|
| 366 |
+
w(0,'RIFF');v.setUint32(4,36+n*2,true);w(8,'WAVE');w(12,'fmt ');v.setUint32(16,16,true);v.setUint16(20,1,true);v.setUint16(22,1,true);
|
| 367 |
+
v.setUint32(24,sr,true);v.setUint32(28,sr*2,true);v.setUint16(32,2,true);v.setUint16(34,16,true);w(36,'data');v.setUint32(40,n*2,true);
|
| 368 |
+
let o=44;for(const c of chunks)for(let i=0;i<c.length;i++){v.setInt16(o,c[i],true);o+=2;}
|
| 369 |
+
return new Blob([buf],{type:'audio/wav'});
|
| 370 |
+
}
|
| 371 |
+
const blobToB64=blob=>new Promise(r=>{const fr=new FileReader();fr.onloadend=()=>r(fr.result.split(',')[1]);fr.readAsDataURL(blob)});
|
| 372 |
+
const fmtT=s=>{if(!isFinite(s)||s<0)s=0;s=Math.floor(s);return `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`};
|
| 373 |
+
const fmtDur=s=>{s=Math.max(0,Math.floor(s));const h=s/3600|0,m=(s%3600/60)|0,x=s%60;return [h,m,x].map(n=>String(n).padStart(2,'0')).join(':')};
|
| 374 |
+
const waveChars=n=>{const c='▁▂▃▄▅▆▇';let s='';for(let i=0;i<n;i++){const v=(Math.sin(i*.7)+Math.sin(i*.31+1.3)+Math.sin(i*1.9+.4))/3;s+=c[Math.max(0,Math.min(c.length-1,Math.floor((v+1)/2*c.length)))];}return s};
|
| 375 |
+
|
| 376 |
+
function attachProg(audio,el,durHint,initPlayed){
|
| 377 |
+
const raw=el.textContent;if(!raw)return;
|
| 378 |
+
el.innerHTML='';el.classList.add('prog');
|
| 379 |
+
const chars=[...raw],wts=chars.map(c=>/\s/.test(c)?.2:/[,、;;::,]/.test(c)?.5:/[.。!!??…—]/.test(c)?1:1);
|
| 380 |
+
const cum=[0];for(const w of wts)cum.push(cum[cum.length-1]+w);
|
| 381 |
+
const tot=cum[cum.length-1]||1;
|
| 382 |
+
const spans=chars.map(c=>{const s=document.createElement('span');s.className='tk';s.textContent=c;el.appendChild(s);return s});
|
| 383 |
+
if(initPlayed)spans.forEach(s=>s.classList.add('played'));
|
| 384 |
+
const dur=()=>isFinite(audio.duration)&&audio.duration>0?audio.duration:(durHint||0);
|
| 385 |
+
const tOf=i=>dur()*cum[i]/tot;
|
| 386 |
+
const findCur=t=>{const d=dur();if(d<=0)return -1;const tg=t/d*tot;let lo=0,hi=spans.length-1;while(lo<hi){const m=(lo+hi+1)>>1;cum[m]<=tg?lo=m:hi=m-1}return lo};
|
| 387 |
+
spans.forEach((s,i)=>s.onclick=()=>{if(!audio.paused&&s.classList.contains('cur')){audio.pause();return}audio.currentTime=tOf(i);if(audio.paused)audio.play()});
|
| 388 |
+
const upd=()=>{const t=audio.currentTime,ci=!audio.paused?findCur(t):-1;spans.forEach((s,i)=>{s.classList.toggle('played',tOf(i)<=t);s.classList.toggle('cur',i===ci)});el.classList.toggle('on',!audio.paused)};
|
| 389 |
+
['timeupdate','seeked','play','pause'].forEach(e=>audio.addEventListener(e,upd));
|
| 390 |
+
}
|
| 391 |
+
function mkPlayer(src){
|
| 392 |
+
const w=document.createElement('div');w.className='player';
|
| 393 |
+
w.innerHTML='<button class="pp">▶</button><div class="bar-wrap"><div class="bar"></div></div><span class="time">0:00 / 0:00</span>';
|
| 394 |
+
const a=new Audio(src),pp=w.querySelector('.pp'),bw=w.querySelector('.bar-wrap'),b=w.querySelector('.bar'),tm=w.querySelector('.time');
|
| 395 |
+
pp.onclick=()=>a.paused?a.play():a.pause();
|
| 396 |
+
a.onplay=()=>pp.textContent='❚❚';a.onpause=()=>pp.textContent='▶';
|
| 397 |
+
a.ontimeupdate=()=>{const d=a.duration||0;b.style.width=d?(a.currentTime/d*100)+'%':'0';tm.textContent=`${fmtT(a.currentTime)} / ${fmtT(d)}`};
|
| 398 |
+
a.onended=()=>b.style.width='0';a.onloadedmetadata=()=>tm.textContent=`0:00 / ${fmtT(a.duration)}`;
|
| 399 |
+
bw.onclick=e=>{const r=bw.getBoundingClientRect();if(a.duration)a.currentTime=(e.clientX-r.left)/r.width*a.duration};
|
| 400 |
+
return w;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
let msgN=0;
|
| 404 |
+
function addMsg(root,role,opts={}){
|
| 405 |
+
const d=document.createElement('div');d.className='msg '+role;msgN++;
|
| 406 |
+
const ts=new Date().toLocaleTimeString('en-US',{hour12:false});
|
| 407 |
+
d.innerHTML=`<div class="meta">[${msgN}] ${role} @ ${ts}</div><div class="body"></div>`;
|
| 408 |
+
const body=d.querySelector('.body');
|
| 409 |
+
if(opts.image){const i=document.createElement('img');i.className='img';i.src=opts.image;body.appendChild(i)}
|
| 410 |
+
if(opts.audioUrl){
|
| 411 |
+
if(role==='user'&&opts.audioDuration){
|
| 412 |
+
const n=Math.max(14,Math.min(80,Math.round(opts.audioDuration/.15)));
|
| 413 |
+
const v=document.createElement('div');v.className='text voice';v.textContent=waveChars(n);body.appendChild(v);
|
| 414 |
+
attachProg(new Audio(opts.audioUrl),v,opts.audioDuration);
|
| 415 |
+
} else body.appendChild(mkPlayer(opts.audioUrl));
|
| 416 |
+
}
|
| 417 |
+
if(opts.text!==undefined){const t=document.createElement('div');t.className='text';t.textContent=opts.text;body.appendChild(t)}
|
| 418 |
+
root.appendChild(d);root.scrollTop=root.scrollHeight;
|
| 419 |
+
return d;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
/* ------------- Chat mode ------------- */
|
| 423 |
+
let chatHist=[],pendImg=null,pendAud=null,rec=null,recChunks=[],recording=false,gen=false,actx=null,nextT=0,aborter=null,chatPlaying=[],chatEpoch=0;
|
| 424 |
+
function stopChatAudio(){for(const s of chatPlaying)try{s.stop()}catch{}chatPlaying=[];if(actx){actx.close().catch(()=>{});actx=null;nextT=0}}
|
| 425 |
+
function stopChat(){chatEpoch++;if(aborter)try{aborter.abort()}catch{}stopChatAudio();gen=false;sendBtn.disabled=false;ts.textContent='idle'}
|
| 426 |
+
|
| 427 |
+
function renderChips(){
|
| 428 |
+
att.innerHTML='';txt.disabled=!!pendAud;txt.placeholder=pendAud?'(voice attached — text disabled)':'Message MiniMind-O...';
|
| 429 |
+
if(pendImg){const c=document.createElement('span');c.className='chip';c.innerHTML=`<img src="${pendImg.url}"><span>image</span><span class="x">×</span>`;c.querySelector('.x').onclick=()=>{URL.revokeObjectURL(pendImg.url);pendImg=null;renderChips()};att.appendChild(c)}
|
| 430 |
+
if(pendAud){const c=document.createElement('span');c.className='chip';c.innerHTML=`<span class="play">▶ voice · ${pendAud.duration.toFixed(1)}s</span><span class="x">×</span>`;c.querySelector('.play').onclick=()=>new Audio(pendAud.url).play();c.querySelector('.x').onclick=()=>{URL.revokeObjectURL(pendAud.url);pendAud=null;renderChips()};att.appendChild(c)}
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
async function send(){
|
| 434 |
+
if(gen)return;
|
| 435 |
+
stopChatAudio();
|
| 436 |
+
const t=txt.value.trim();if(!t&&!pendImg&&!pendAud)return;
|
| 437 |
+
const o={};if(pendImg)o.image=pendImg.url;if(pendAud){o.audioUrl=pendAud.url;o.audioDuration=pendAud.duration}if(t)o.text=t;
|
| 438 |
+
addMsg(chat,'user',o);
|
| 439 |
+
const payload={text:t||'',audio:pendAud?.b64||null,image:pendImg?.b64||null,temperature:.7,max_tokens:512,history:chatHist,voice:voiceSel.value};
|
| 440 |
+
txt.value='';pendImg=null;pendAud=null;renderChips();
|
| 441 |
+
sendBtn.disabled=true;gen=true;stat.classList.remove('hidden');tt.textContent='—';ta.textContent='—';ts.textContent='sending';
|
| 442 |
+
const ad=addMsg(chat,'assistant',{text:'…'}),ab=ad.querySelector('.body'),at=ab.querySelector('.text');
|
| 443 |
+
let full='',segs=[],acc='';
|
| 444 |
+
try{
|
| 445 |
+
const myEpoch=++chatEpoch;
|
| 446 |
+
aborter=new AbortController();
|
| 447 |
+
const res=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),signal:aborter.signal});
|
| 448 |
+
const rd=res.body.getReader(),dec=new TextDecoder();ts.textContent='streaming';
|
| 449 |
+
while(true){
|
| 450 |
+
if(myEpoch!==chatEpoch)throw new DOMException('chat stopped','AbortError');
|
| 451 |
+
const {done,value}=await rd.read();if(done)break;
|
| 452 |
+
for(const ln of dec.decode(value).split('\n')){
|
| 453 |
+
if(myEpoch!==chatEpoch)throw new DOMException('chat stopped','AbortError');
|
| 454 |
+
if(!ln.startsWith('data: '))continue;
|
| 455 |
+
let d;try{d=JSON.parse(ln.slice(6))}catch{continue}
|
| 456 |
+
if(d.type==='user_prompt')chatHist.push({role:'user',content:d.content});
|
| 457 |
+
else if(d.type==='ttft'){if(d.text_ttft!==undefined)tt.textContent=d.text_ttft+' ms';if(d.audio_ttft!==undefined)ta.textContent=d.audio_ttft+' ms'}
|
| 458 |
+
else if(d.type==='text'){full+=d.content;at.textContent=full;chat.scrollTop=chat.scrollHeight}
|
| 459 |
+
else if(d.type==='pcm'){
|
| 460 |
+
acc+=d.c;
|
| 461 |
+
if(d.d){
|
| 462 |
+
if(!actx){actx=new AudioContext({sampleRate:24000});nextT=actx.currentTime+.1}
|
| 463 |
+
const {i16,f32}=decPcm(acc);
|
| 464 |
+
const buf=actx.createBuffer(1,f32.length,24000);buf.getChannelData(0).set(f32);
|
| 465 |
+
const s=actx.createBufferSource();s.buffer=buf;s.connect(actx.destination);
|
| 466 |
+
s.onended=()=>{chatPlaying=chatPlaying.filter(x=>x!==s)};
|
| 467 |
+
chatPlaying.push(s);
|
| 468 |
+
if(nextT<actx.currentTime)nextT=actx.currentTime;
|
| 469 |
+
s.start(nextT);nextT+=buf.duration-.01;segs.push(i16);acc='';
|
| 470 |
+
}
|
| 471 |
+
} else if(d.type==='error')at.textContent='⚠ '+d.content;
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
if(segs.length){const url=URL.createObjectURL(buildWav(segs,24000));attachProg(new Audio(url),at,null,true)}
|
| 475 |
+
if(full)chatHist.push({role:'assistant',content:full});
|
| 476 |
+
ts.textContent='done';
|
| 477 |
+
} catch(e){if(e.name!=='AbortError'){at.textContent='⚠ '+e.message;ts.textContent='error'}}
|
| 478 |
+
gen=false;sendBtn.disabled=false;aborter=null;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
sendBtn.onclick=send;
|
| 482 |
+
txt.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
|
| 483 |
+
const imgPop=$('imgPop'),fileBtn=$('fileBtn'),shotBtn=$('shotBtn'),snap=$('snap'),snapV=$('snapV'),snapOk=$('snapOk'),snapCancel=$('snapCancel');
|
| 484 |
+
const cloneModal=$('cloneModal'),cloneText=$('cloneText'),cloneStat=$('cloneStat'),cloneRecBtn=$('cloneRec'),cloneCancelBtn=$('cloneCancel'),cloneName=$('cloneName');
|
| 485 |
+
cloneText.textContent=CLONE_PROMPT;
|
| 486 |
+
moreBtn.onclick=e=>{e.stopPropagation();morePop.classList.toggle('on')};
|
| 487 |
+
morePop.onclick=e=>{
|
| 488 |
+
e.stopPropagation();
|
| 489 |
+
const del=e.target.closest('.vx');
|
| 490 |
+
if(del){const name=del.dataset.del;if(!confirm('确认删除 '+name+'?'))return;fetch('/delete_voice',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})}).then(r=>r.json()).then(d=>{if(!d.ok)throw new Error(d.error);loadVoices(voiceSel.value===name?'default':voiceSel.value)}).catch(e=>alert(e.message));return}
|
| 491 |
+
const add=e.target.closest('.vadd');
|
| 492 |
+
if(add){closeMore();stopChat();cloneModal.classList.add('on');if(!cloneName.value.trim())cloneName.value='voice_clone';cloneStat.textContent='请自然读完,不用刻意加速';return}
|
| 493 |
+
const item=e.target.closest('.vitem');
|
| 494 |
+
if(item&&item.dataset.voice){voiceSel.value=item.dataset.voice;renderVoiceList();closeMore()}
|
| 495 |
+
};
|
| 496 |
+
imgBtn.onclick=e=>{e.stopPropagation();imgPop.classList.toggle('on')};
|
| 497 |
+
document.addEventListener('click',e=>{if(!imgPop.contains(e.target)&&e.target!==imgBtn)imgPop.classList.remove('on');if(!morePop.contains(e.target)&&e.target!==moreBtn)closeMore()});
|
| 498 |
+
fileBtn.onclick=()=>{imgPop.classList.remove('on');imgIn.click()};
|
| 499 |
+
const setImg=(b64,url)=>{if(pendImg)URL.revokeObjectURL(pendImg.url);pendImg={b64,url};renderChips()};
|
| 500 |
+
imgIn.onchange=()=>{const f=imgIn.files?.[0];if(!f)return;const r=new FileReader();r.onloadend=()=>setImg(r.result.split(',')[1],URL.createObjectURL(f));r.readAsDataURL(f);imgIn.value=''};
|
| 501 |
+
let snapStream=null;
|
| 502 |
+
const closeSnap=()=>{snap.classList.remove('on');if(snapStream){snapStream.getTracks().forEach(t=>t.stop());snapStream=null}snapV.srcObject=null};
|
| 503 |
+
shotBtn.onclick=async()=>{
|
| 504 |
+
imgPop.classList.remove('on');
|
| 505 |
+
try{snapStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:'user',width:1280,height:720}});snapV.srcObject=snapStream;snap.classList.add('on')}
|
| 506 |
+
catch(e){alert('camera error: '+e.message)}
|
| 507 |
+
};
|
| 508 |
+
snapCancel.onclick=closeSnap;
|
| 509 |
+
snapOk.onclick=()=>{
|
| 510 |
+
if(!snapV.videoWidth)return;
|
| 511 |
+
const cv=document.createElement('canvas');cv.width=snapV.videoWidth;cv.height=snapV.videoHeight;cv.getContext('2d').drawImage(snapV,0,0);
|
| 512 |
+
const dataUrl=cv.toDataURL('image/jpeg',.9),b64=dataUrl.split(',')[1];
|
| 513 |
+
cv.toBlob(blob=>setImg(b64,URL.createObjectURL(blob)),'image/jpeg',.9);
|
| 514 |
+
closeSnap();
|
| 515 |
+
};
|
| 516 |
+
const CLONE_MAX_SECS=6;
|
| 517 |
+
let cloneRec=null,cloneChunks=[],cloneRecording=false,cloneBusy=false,cloneCanceled=false,cloneStream=null,cloneDeadline=0,cloneTimer=null;
|
| 518 |
+
const stopCloneTimer=()=>{if(cloneTimer){clearInterval(cloneTimer);cloneTimer=null}};
|
| 519 |
+
const resetClone=()=>{stopCloneTimer();if(cloneStream){cloneStream.getTracks().forEach(t=>t.stop());cloneStream=null}cloneRec=null;cloneChunks=[];cloneRecording=false;cloneBusy=false;cloneCanceled=false;cloneDeadline=0;cloneRecBtn.classList.remove('rec');cloneRecBtn.textContent='start';cloneRecBtn.disabled=false;cloneCancelBtn.disabled=false};
|
| 520 |
+
const hideClone=()=>{cloneModal.classList.remove('on');resetClone();cloneStat.textContent='点击 start 开始录音'};
|
| 521 |
+
cloneCancelBtn.onclick=()=>{if(cloneBusy)return;if(cloneRecording&&cloneRec){cloneCanceled=true;cloneStat.textContent='已取消';cloneRec.stop()}else hideClone()};
|
| 522 |
+
cloneRecBtn.onclick=async()=>{
|
| 523 |
+
if(cloneBusy)return;
|
| 524 |
+
if(!cloneRecording){
|
| 525 |
+
try{
|
| 526 |
+
cloneStream=await navigator.mediaDevices.getUserMedia({audio:true});
|
| 527 |
+
cloneRec=new MediaRecorder(cloneStream);cloneChunks=[];cloneCanceled=false;
|
| 528 |
+
cloneRec.ondataavailable=e=>cloneChunks.push(e.data);
|
| 529 |
+
cloneRec.onstop=async()=>{
|
| 530 |
+
const canceled=cloneCanceled,blob=new Blob(cloneChunks,{type:(cloneRec&&cloneRec.mimeType)||'audio/webm'});
|
| 531 |
+
resetClone();
|
| 532 |
+
if(canceled){cloneModal.classList.remove('on');cloneStat.textContent='点击 start 开始录音';return}
|
| 533 |
+
cloneBusy=true;cloneRecBtn.disabled=true;cloneCancelBtn.disabled=true;cloneStat.textContent='正在提取音色…';
|
| 534 |
+
try{
|
| 535 |
+
const audio=await blobToB64(blob);
|
| 536 |
+
const res=await fetch('/clone_voice',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({audio,name:cloneName.value.trim()})});
|
| 537 |
+
const d=await res.json();
|
| 538 |
+
if(!res.ok||!d.ok)throw new Error(d.error||'clone failed');
|
| 539 |
+
await loadVoices(d.voice);
|
| 540 |
+
voiceSel.value=d.voice;
|
| 541 |
+
cloneStat.textContent='音色已生成,已自动切换到 '+d.voice;
|
| 542 |
+
setTimeout(()=>hideClone(),600);
|
| 543 |
+
}catch(e){cloneBusy=false;cloneRecBtn.disabled=false;cloneCancelBtn.disabled=false;cloneStat.textContent='失败:'+e.message}
|
| 544 |
+
};
|
| 545 |
+
cloneRec.start();cloneRecording=true;cloneRecBtn.classList.add('rec');cloneRecBtn.textContent='stop';cloneDeadline=Date.now()+CLONE_MAX_SECS*1000;
|
| 546 |
+
const tick=()=>{
|
| 547 |
+
const left=Math.max(0,Math.ceil((cloneDeadline-Date.now())/1000));
|
| 548 |
+
cloneStat.textContent=`正在录音,请读完这句话 · ${left}s`;
|
| 549 |
+
if(left<=0&&cloneRecording&&cloneRec){cloneBusy=true;cloneRecording=false;cloneRecBtn.disabled=true;cloneRecBtn.classList.remove('rec');cloneRecBtn.textContent='start';cloneStat.textContent='录音结束,正在上传…';cloneRec.stop()}
|
| 550 |
+
};
|
| 551 |
+
tick();
|
| 552 |
+
cloneTimer=setInterval(tick,200);
|
| 553 |
+
}catch(e){alert('mic error: '+e.message)}
|
| 554 |
+
}else{
|
| 555 |
+
stopCloneTimer();cloneBusy=true;cloneRecording=false;cloneRecBtn.disabled=true;cloneRecBtn.classList.remove('rec');cloneRecBtn.textContent='start';cloneStat.textContent='录音结束,正在上传…';cloneRec.stop();
|
| 556 |
+
}
|
| 557 |
+
};
|
| 558 |
+
recBtn.onclick=async()=>{
|
| 559 |
+
if(!recording){
|
| 560 |
+
try{
|
| 561 |
+
const st=await navigator.mediaDevices.getUserMedia({audio:true});
|
| 562 |
+
rec=new MediaRecorder(st);recChunks=[];const t0=Date.now();
|
| 563 |
+
rec.ondataavailable=e=>recChunks.push(e.data);
|
| 564 |
+
rec.onstop=()=>{
|
| 565 |
+
st.getTracks().forEach(t=>t.stop());
|
| 566 |
+
const blob=new Blob(recChunks,{type:'audio/webm'}),dur=(Date.now()-t0)/1000,r=new FileReader();
|
| 567 |
+
r.onloadend=()=>{const b64=r.result.split(',')[1],url=URL.createObjectURL(blob);if(pendAud)URL.revokeObjectURL(pendAud.url);pendAud={b64,url,duration:dur};renderChips()};
|
| 568 |
+
r.readAsDataURL(blob);
|
| 569 |
+
};
|
| 570 |
+
rec.start();recording=true;recBtn.classList.add('rec');
|
| 571 |
+
} catch(e){alert('mic error: '+e.message)}
|
| 572 |
+
} else {rec.stop();recording=false;recBtn.classList.remove('rec')}
|
| 573 |
+
};
|
| 574 |
+
|
| 575 |
+
/* ------------- Call mode ------------- */
|
| 576 |
+
let callInit=false,callStop=null;
|
| 577 |
+
callBtn.onclick=openCall;
|
| 578 |
+
function openCall(){stopChat();document.body.classList.add('call');history.pushState({call:1},'','/call');if(!callInit){callInit=true;initCall()}}
|
| 579 |
+
function closeCall(){document.body.classList.remove('call');if(callStop)callStop();if(location.pathname==='/call')history.pushState({},'','/')}
|
| 580 |
+
window.addEventListener('popstate',()=>location.pathname==='/call'?openCall():closeCall());
|
| 581 |
+
if(location.pathname==='/call')openCall();
|
| 582 |
+
|
| 583 |
+
function initCall(){
|
| 584 |
+
const stT=$('stT'),upT=$('upT'),stIc=$('stIc'),lbl=$('lbl'),osvg=$('osvg'),core=$('core'),ripG=$('rip');
|
| 585 |
+
const ICONS={
|
| 586 |
+
idle:'<svg viewBox="0 0 24 24"><circle class="dot" cx="12" cy="12" r="3" fill="currentColor" stroke="none"/></svg>',
|
| 587 |
+
listen:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.2" fill="currentColor" stroke="none"/><path class="w w3" d="M7 8.5a6 6 0 0 0 0 7"/><path class="w w3" d="M17 8.5a6 6 0 0 1 0 7"/><path class="w w2" d="M4 6a9.5 9.5 0 0 0 0 12"/><path class="w w2" d="M20 6a9.5 9.5 0 0 1 0 12"/></svg>',
|
| 588 |
+
gen:'<svg viewBox="0 0 24 24"><circle cx="12" cy="4" r="1.8" fill="currentColor" stroke="none"/><circle cx="18.93" cy="15.5" r="1.4" fill="currentColor" stroke="none" opacity=".65"/><circle cx="5.07" cy="15.5" r="1" fill="currentColor" stroke="none" opacity=".35"/></svg>',
|
| 589 |
+
speak:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.2" fill="currentColor" stroke="none"/><path class="a1" d="M8 9a5 5 0 0 1 0 6"/><path class="a1" d="M16 9a5 5 0 0 0 0 6"/><path class="a2" d="M5 6.5a9 9 0 0 1 0 11"/><path class="a2" d="M19 6.5a9 9 0 0 0 0 11"/><path class="a3" d="M2 4a13 13 0 0 1 0 16"/><path class="a3" d="M22 4a13 13 0 0 0 0 16"/></svg>',
|
| 590 |
+
err:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="8.5" y1="8.5" x2="15.5" y2="15.5"/><line x1="15.5" y1="8.5" x2="8.5" y2="15.5"/></svg>',
|
| 591 |
+
};
|
| 592 |
+
const cchat=$('cchat'),lat=$('lat'),lmE=$('lmE'),ltE=$('ltE');
|
| 593 |
+
const camBtn=$('camBtn'),logBtn=$('logBtn'),endBtn=$('endBtn'),orb=$('orb'),camV=$('camV');
|
| 594 |
+
const pill=$('pill'),connT=$('connT'),tcnt=$('tcnt'),trans=$('trans'),closeBtn=$('closeBtn');
|
| 595 |
+
const blobs=[$('b0'),$('b1'),$('b2')];
|
| 596 |
+
|
| 597 |
+
let ac,ana,mic,camStream=null,camOn=false,hist=[],nextT=0;
|
| 598 |
+
let ws=null,wsActive=false,proc=null,rtc=null,rtSrc=null,playing=[],turns=0,mn=0;
|
| 599 |
+
let curMsg=null,curText='',segs=[],start=0,ripT=0,sVol=0,asstPlay=false,raf=0;
|
| 600 |
+
let spk=false,userBuf=[];
|
| 601 |
+
const RT_SAMPLE_RATE=16000,RT_BLOCK=4096;
|
| 602 |
+
const resamplePcm=(input,inRate,outRate)=>{
|
| 603 |
+
if(!input.length||inRate===outRate)return input;
|
| 604 |
+
const outLen=Math.max(1,Math.round(input.length*outRate/inRate));
|
| 605 |
+
const out=new Float32Array(outLen);
|
| 606 |
+
const step=inRate/outRate;
|
| 607 |
+
for(let i=0;i<outLen;i++){
|
| 608 |
+
const pos=i*step,idx=Math.floor(pos),frac=pos-idx;
|
| 609 |
+
const a=input[idx]||0;
|
| 610 |
+
const b=input[Math.min(idx+1,input.length-1)]||a;
|
| 611 |
+
out[i]=a+(b-a)*frac;
|
| 612 |
+
}
|
| 613 |
+
return out;
|
| 614 |
+
};
|
| 615 |
+
const floatToI16=input=>{const out=new Int16Array(input.length);for(let i=0;i<input.length;i++)out[i]=Math.max(-32768,Math.min(32767,input[i]*32768));return out};
|
| 616 |
+
const flushUser=()=>{
|
| 617 |
+
if(!userBuf.length)return;
|
| 618 |
+
const dur=userBuf.reduce((a,c)=>a+c.length,0)/16000;
|
| 619 |
+
const url=URL.createObjectURL(buildWav(userBuf,16000));
|
| 620 |
+
userBuf=[];
|
| 621 |
+
const d=addCM('user'),body=d.querySelector('.body'),n=Math.max(14,Math.min(80,Math.round(dur/.15)));
|
| 622 |
+
const v=document.createElement('div');v.className='text voice';v.textContent=waveChars(n);body.appendChild(v);
|
| 623 |
+
attachProg(new Audio(url),v,dur);
|
| 624 |
+
setLatest('user',`[voice · ${dur.toFixed(1)}s]`);
|
| 625 |
+
};
|
| 626 |
+
|
| 627 |
+
const N=32,NC=2,NR=5;let shT=0;
|
| 628 |
+
const LAY=[{c:56,r:112,a:1.8,sp:.0005,ph:.6,vc:34,vr:18},{c:80,r:124,a:2.6,sp:.0008,ph:1.3,vc:52,vr:26},{c:104,r:138,a:3.6,sp:.0011,ph:2.1,vc:74,vr:34}];
|
| 629 |
+
|
| 630 |
+
const catmull=pts=>{let d=`M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)} `;const n=pts.length;for(let i=0;i<n;i++){const p0=pts[(i-1+n)%n],p1=pts[i],p2=pts[(i+1)%n],p3=pts[(i+2)%n];const c1x=p1[0]+(p2[0]-p0[0])/6,c1y=p1[1]+(p2[1]-p0[1])/6,c2x=p2[0]-(p3[0]-p1[0])/6,c2y=p2[1]-(p3[1]-p1[1])/6;d+=`C ${c1x.toFixed(2)} ${c1y.toFixed(2)}, ${c2x.toFixed(2)} ${c2y.toFixed(2)}, ${p2[0].toFixed(2)} ${p2[1].toFixed(2)} `}return d+'Z'};
|
| 631 |
+
const setSt=(cls,t)=>{osvg.setAttribute('class',cls||'');lbl.className='lbl '+(cls||'');stT.textContent=t;const key=cls||(/error/i.test(t)?'err':'idle');stIc.innerHTML=ICONS[key]||ICONS.idle};
|
| 632 |
+
const setConn=(live,t)=>{pill.className='pill'+(live?' live':'');connT.textContent=t};
|
| 633 |
+
const ripple=k=>{const now=performance.now();if(now-ripT<550)return;ripT=now;const c=document.createElementNS('http://www.w3.org/2000/svg','circle');c.setAttribute('cx',0);c.setAttribute('cy',0);c.setAttribute('r',85);c.setAttribute('class','ripple '+k);ripG.appendChild(c);c.addEventListener('animationend',()=>c.remove())};
|
| 634 |
+
const upTimer=setInterval(()=>{if(start)upT.textContent=fmtDur((Date.now()-start)/1000)},1000);
|
| 635 |
+
|
| 636 |
+
const setLatest=(role,t)=>{if(!t)return;lat.classList.remove('empty','user','assistant');lat.classList.add(role);const ts=new Date().toLocaleTimeString('en-US',{hour12:false});lmE.textContent=`latest · ${role} @ ${ts}`;ltE.textContent=t;lat.scrollTop=0};
|
| 637 |
+
const addCM=role=>{const d=document.createElement('div');d.className='msg '+role;mn++;const ts=new Date().toLocaleTimeString('en-US',{hour12:false});d.innerHTML=`<div class="meta">[${mn}] ${role} @ ${ts}</div><div class="body"></div>`;cchat.appendChild(d);cchat.scrollTop=cchat.scrollHeight;return d};
|
| 638 |
+
const playPcm=b64=>{const {i16,f32}=decPcm(b64);segs.push(i16);const buf=ac.createBuffer(1,f32.length,24000);buf.getChannelData(0).set(f32);const s=ac.createBufferSource();s.buffer=buf;s.connect(ac.destination);s.onended=()=>{playing=playing.filter(x=>x!==s);if(!playing.length){asstPlay=false;if(!curMsg)setSt('','idle')}};playing.push(s);asstPlay=true;if(nextT<ac.currentTime)nextT=ac.currentTime;s.start(nextT);nextT+=buf.duration-.01};
|
| 639 |
+
const stopPcm=()=>{for(const s of playing)try{s.stop()}catch{}playing=[];asstPlay=false;if(ac)nextT=ac.currentTime};
|
| 640 |
+
const capCam=()=>{if(!camOn||!camV.videoWidth)return null;const cv=document.createElement('canvas');cv.width=camV.videoWidth;cv.height=camV.videoHeight;cv.getContext('2d').drawImage(camV,0,0);return cv.toDataURL('image/jpeg',.8).split(',')[1]};
|
| 641 |
+
const sendCtx=()=>{if(!ws||ws.readyState!==1)return;ws.send(JSON.stringify({type:'context',history:hist,image:camOn?capCam():null,voice:callVoiceSel.value}))};
|
| 642 |
+
window.callSendCtx=sendCtx;
|
| 643 |
+
|
| 644 |
+
function render(){
|
| 645 |
+
const now=performance.now();let vol=0,fq=null;
|
| 646 |
+
if(ana){const d=new Uint8Array(ana.frequencyBinCount);ana.getByteFrequencyData(d);fq=d;let s=0;for(let i=0;i<d.length;i++)s+=d[i];vol=s/d.length/255}
|
| 647 |
+
if(asstPlay)vol=Math.max(vol,.6+.25*Math.sin(now*.013));
|
| 648 |
+
sVol=sVol*.7+vol*.3;
|
| 649 |
+
const tgt=camOn?1:0;shT+=(tgt-shT)*.08;
|
| 650 |
+
const shN=NC+(NR-NC)*shT,inv=-1/shN;
|
| 651 |
+
for(let li=0;li<LAY.length;li++){
|
| 652 |
+
const L=LAY[li],hw=L.c+(L.r-L.c)*shT,hh=hw,vs=L.vc+(L.vr-L.vc)*shT;
|
| 653 |
+
const pts=new Array(N);
|
| 654 |
+
for(let i=0;i<N;i++){
|
| 655 |
+
const ang=(i/N)*Math.PI*2,cA=Math.cos(ang),sA=Math.sin(ang);
|
| 656 |
+
const n=Math.sin(now*L.sp+i*L.ph)+.55*Math.sin(now*L.sp*1.7+i*L.ph*.7+li);
|
| 657 |
+
let fv=0;if(fq){const idx=Math.floor((i/N)*Math.min(64,fq.length))+li*4;fv=(fq[idx%fq.length]||0)/255}
|
| 658 |
+
if(asstPlay)fv=Math.max(fv,.5+.45*Math.abs(Math.sin(now*.011+i*.55+li*.9)));
|
| 659 |
+
const g=Math.min(1,fv*.65+sVol*.55);
|
| 660 |
+
const tx=Math.pow(Math.abs(cA)/hw,shN),ty=Math.pow(Math.abs(sA)/hh,shN);
|
| 661 |
+
const r=Math.pow(tx+ty,inv)+n*L.a+g*vs;
|
| 662 |
+
pts[i]=[cA*r,sA*r];
|
| 663 |
+
}
|
| 664 |
+
blobs[li].setAttribute('d',catmull(pts));
|
| 665 |
+
}
|
| 666 |
+
core.setAttribute('r',(3+sVol*10).toFixed(1));
|
| 667 |
+
raf=requestAnimationFrame(render);
|
| 668 |
+
}
|
| 669 |
+
raf=requestAnimationFrame(render);
|
| 670 |
+
|
| 671 |
+
async function initMic(){
|
| 672 |
+
try{mic=await navigator.mediaDevices.getUserMedia({audio:true});ac=new AudioContext();const s=ac.createMediaStreamSource(mic);ana=ac.createAnalyser();ana.fftSize=256;s.connect(ana);setSt('','connecting');setConn(false,'connecting')}
|
| 673 |
+
catch(e){setSt('','mic error');setConn(false,'mic error')}
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
camBtn.onclick=async()=>{
|
| 677 |
+
if(camOn){if(camStream){camStream.getTracks().forEach(t=>t.stop());camStream=null}camV.srcObject=null;orb.classList.remove('camon');camBtn.classList.remove('active');camOn=false}
|
| 678 |
+
else{try{camStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:'user',width:640,height:480}});camV.srcObject=camStream;orb.classList.add('camon');camBtn.classList.add('active');camOn=true}catch{setSt('','cam error')}}
|
| 679 |
+
if(wsActive)sendCtx();
|
| 680 |
+
};
|
| 681 |
+
logBtn.onclick=()=>{trans.classList.toggle('on');logBtn.classList.toggle('active',trans.classList.contains('on'))};
|
| 682 |
+
closeBtn.onclick=()=>{trans.classList.remove('on');logBtn.classList.remove('active')};
|
| 683 |
+
endBtn.onclick=closeCall;
|
| 684 |
+
|
| 685 |
+
async function startRT(){
|
| 686 |
+
wsActive=true;if(!mic)await initMic();
|
| 687 |
+
rtc=new AudioContext();rtSrc=rtc.createMediaStreamSource(mic);proc=rtc.createScriptProcessor(4096,1,1);
|
| 688 |
+
let rtAcc=new Float32Array(0);
|
| 689 |
+
const pr=location.protocol==='https:'?'wss':'ws';
|
| 690 |
+
ws=new WebSocket(`${pr}://${location.host}/ws/realtime`);ws.binaryType='arraybuffer';
|
| 691 |
+
ws.onmessage=e=>{
|
| 692 |
+
const m=JSON.parse(e.data);
|
| 693 |
+
if(m.type==='vad'){
|
| 694 |
+
if(m.speaking&&!spk){sendCtx();stopPcm();ripple('user');userBuf=[]}
|
| 695 |
+
if(!m.speaking&&spk)flushUser();
|
| 696 |
+
spk=m.speaking;
|
| 697 |
+
if(m.speaking)setSt('listen','listening');else if(!asstPlay&&!curMsg)setSt('','idle');
|
| 698 |
+
} else if(m.type==='generating'){
|
| 699 |
+
setSt('gen','thinking');if(spk){flushUser();spk=false}
|
| 700 |
+
turns++;tcnt.textContent=turns+(turns===1?' turn':' turns');
|
| 701 |
+
curText='';segs=[];
|
| 702 |
+
const d=addCM('assistant'),body=d.querySelector('.body'),t=document.createElement('div');t.className='text';body.appendChild(t);
|
| 703 |
+
curMsg={div:d,body,textEl:t};stopPcm();
|
| 704 |
+
} else if(m.type==='text'){
|
| 705 |
+
curText+=m.content;if(curMsg)curMsg.textEl.textContent=curText;
|
| 706 |
+
setLatest('assistant',curText);cchat.scrollTop=cchat.scrollHeight;
|
| 707 |
+
} else if(m.type==='pcm'){setSt('speak','speaking');ripple('assistant');playPcm(m.data)}
|
| 708 |
+
else if(m.type==='done'){
|
| 709 |
+
if(curText)hist.push({role:'assistant',content:curText});
|
| 710 |
+
if(m.interrupted)stopPcm();
|
| 711 |
+
if(curMsg&&segs.length){const url=URL.createObjectURL(buildWav(segs,24000));attachProg(new Audio(url),curMsg.textEl,null,true)}
|
| 712 |
+
curMsg=null;if(!asstPlay)setSt('','idle');
|
| 713 |
+
}
|
| 714 |
+
};
|
| 715 |
+
ws.onclose=()=>{if(wsActive)stopRT()};
|
| 716 |
+
ws.onopen=()=>{
|
| 717 |
+
start=Date.now();setConn(true,'live');setSt('','idle');sendCtx();
|
| 718 |
+
proc.onaudioprocess=e=>{
|
| 719 |
+
if(ws.readyState!==1)return;
|
| 720 |
+
const rs=resamplePcm(e.inputBuffer.getChannelData(0),e.inputBuffer.sampleRate,RT_SAMPLE_RATE);
|
| 721 |
+
const merged=new Float32Array(rtAcc.length+rs.length);merged.set(rtAcc);merged.set(rs,rtAcc.length);
|
| 722 |
+
let off=0;
|
| 723 |
+
for(;merged.length-off>=RT_BLOCK;off+=RT_BLOCK){
|
| 724 |
+
const i16=floatToI16(merged.subarray(off,off+RT_BLOCK));
|
| 725 |
+
ws.send(i16.buffer);
|
| 726 |
+
if(spk)userBuf.push(new Int16Array(i16));
|
| 727 |
+
}
|
| 728 |
+
rtAcc=merged.slice(off);
|
| 729 |
+
};
|
| 730 |
+
rtSrc.connect(proc);proc.connect(rtc.destination);
|
| 731 |
+
};
|
| 732 |
+
}
|
| 733 |
+
function stopRT(){wsActive=false;if(ws){ws.close();ws=null}if(proc){proc.disconnect();proc.onaudioprocess=null;proc=null}if(rtSrc){rtSrc.disconnect();rtSrc=null}if(rtc){rtc.close();rtc=null}stopPcm();setSt('','disconnected');setConn(false,'offline')}
|
| 734 |
+
|
| 735 |
+
callStop=()=>{
|
| 736 |
+
stopRT();
|
| 737 |
+
window.callSendCtx=null;
|
| 738 |
+
if(camStream){camStream.getTracks().forEach(t=>t.stop());camStream=null;camOn=false;orb.classList.remove('camon');camV.srcObject=null;camBtn.classList.remove('active')}
|
| 739 |
+
if(mic){mic.getTracks().forEach(t=>t.stop());mic=null}
|
| 740 |
+
if(ac){try{ac.close()}catch{}ac=null}
|
| 741 |
+
if(raf)cancelAnimationFrame(raf);
|
| 742 |
+
clearInterval(upTimer);callInit=false;callStop=null;
|
| 743 |
+
};
|
| 744 |
+
initMic().then(startRT);
|
| 745 |
+
}
|
| 746 |
+
</script>
|
| 747 |
+
</body>
|
| 748 |
+
</html>
|
|
@@ -1,10 +1,35 @@
|
|
| 1 |
[project]
|
| 2 |
name = "omni"
|
| 3 |
version = "0.1.0"
|
| 4 |
-
description = "
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12"
|
| 7 |
-
dependencies = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
[tool.setuptools.packages.find]
|
| 10 |
where = ["src"]
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "omni"
|
| 3 |
version = "0.1.0"
|
| 4 |
+
description = "MiniMind LLM (and future multimodal) training & inference framework, structured as an installable package."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"torch>=2.6.0",
|
| 9 |
+
"transformers>=4.57.6",
|
| 10 |
+
"datasets>=3.6.0",
|
| 11 |
+
"tokenizers>=0.20.0",
|
| 12 |
+
"tiktoken>=0.10.0",
|
| 13 |
+
"jinja2>=3.1.2",
|
| 14 |
+
"numpy>=1.26.4",
|
| 15 |
+
"safetensors>=0.5.0",
|
| 16 |
+
"accelerate>=1.0.0",
|
| 17 |
+
"sentencepiece>=0.2.0",
|
| 18 |
+
"jieba>=0.42.1",
|
| 19 |
+
"pydantic>=2.11.5",
|
| 20 |
+
"rich>=13.7.1",
|
| 21 |
+
"regex",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[project.optional-dependencies]
|
| 25 |
+
rl = ["swanlab>=0.7.11", "wandb>=0.18.3"]
|
| 26 |
+
serve = ["fastapi>=3.0.3", "uvicorn", "pydantic>=2.11.5", "openai>=1.59.6", "flask>=3.0.3", "flask-cors>=4.0.0"]
|
| 27 |
+
demo = ["streamlit>=1.50.0"]
|
| 28 |
+
all = ["swanlab>=0.7.11", "wandb>=0.18.3", "fastapi>=3.0.3", "uvicorn", "openai>=1.59.6", "flask>=3.0.3", "flask-cors>=4.0.0", "streamlit>=1.50.0", "sglang"]
|
| 29 |
+
|
| 30 |
+
[build-system]
|
| 31 |
+
requires = ["setuptools>=61.0"]
|
| 32 |
+
build-backend = "setuptools.build_meta"
|
| 33 |
|
| 34 |
[tool.setuptools.packages.find]
|
| 35 |
where = ["src"]
|
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.core.norm import RMSNorm
|
| 2 |
+
from omni.core.rope import precompute_freqs_cis, apply_rotary_pos_emb, repeat_kv
|
| 3 |
+
from omni.core.attention import Attention
|
| 4 |
+
from omni.core.mlp import FeedForward, MOEFeedForward
|
| 5 |
+
from omni.core.block import MiniMindBlock
|
| 6 |
+
from omni.core.model import MiniMindModel
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"RMSNorm",
|
| 10 |
+
"precompute_freqs_cis",
|
| 11 |
+
"apply_rotary_pos_emb",
|
| 12 |
+
"repeat_kv",
|
| 13 |
+
"Attention",
|
| 14 |
+
"FeedForward",
|
| 15 |
+
"MOEFeedForward",
|
| 16 |
+
"MiniMindBlock",
|
| 17 |
+
"MiniMindModel",
|
| 18 |
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
from omni.core.norm import RMSNorm
|
| 7 |
+
from omni.core.rope import apply_rotary_pos_emb, repeat_kv
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Attention(nn.Module):
|
| 11 |
+
def __init__(self, config: "MiniMindConfig"):
|
| 12 |
+
super().__init__()
|
| 13 |
+
self.num_key_value_heads = config.num_attention_heads if config.num_key_value_heads is None else config.num_key_value_heads
|
| 14 |
+
self.n_local_heads = config.num_attention_heads
|
| 15 |
+
self.n_local_kv_heads = self.num_key_value_heads
|
| 16 |
+
self.n_rep = self.n_local_heads // self.n_local_kv_heads
|
| 17 |
+
self.head_dim = config.head_dim
|
| 18 |
+
self.is_causal = True
|
| 19 |
+
self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False)
|
| 20 |
+
self.k_proj = nn.Linear(config.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)
|
| 21 |
+
self.v_proj = nn.Linear(config.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)
|
| 22 |
+
self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False)
|
| 23 |
+
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 24 |
+
self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 25 |
+
self.attn_dropout = nn.Dropout(config.dropout)
|
| 26 |
+
self.resid_dropout = nn.Dropout(config.dropout)
|
| 27 |
+
self.dropout = config.dropout
|
| 28 |
+
self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention') and config.flash_attn
|
| 29 |
+
|
| 30 |
+
def forward(self, x, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None):
|
| 31 |
+
bsz, seq_len, _ = x.shape
|
| 32 |
+
xq, xk, xv = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
| 33 |
+
xq = xq.view(bsz, seq_len, self.n_local_heads, self.head_dim)
|
| 34 |
+
xk = xk.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim)
|
| 35 |
+
xv = xv.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim)
|
| 36 |
+
xq, xk = self.q_norm(xq), self.k_norm(xk)
|
| 37 |
+
cos, sin = position_embeddings
|
| 38 |
+
xq, xk = apply_rotary_pos_emb(xq, xk, cos, sin)
|
| 39 |
+
if past_key_value is not None:
|
| 40 |
+
xk = torch.cat([past_key_value[0], xk], dim=1)
|
| 41 |
+
xv = torch.cat([past_key_value[1], xv], dim=1)
|
| 42 |
+
past_kv = (xk, xv) if use_cache else None
|
| 43 |
+
xq, xk, xv = (xq.transpose(1, 2), repeat_kv(xk, self.n_rep).transpose(1, 2), repeat_kv(xv, self.n_rep).transpose(1, 2))
|
| 44 |
+
if self.flash and (seq_len > 1) and (not self.is_causal or past_key_value is None) and (attention_mask is None or torch.all(attention_mask == 1)):
|
| 45 |
+
output = F.scaled_dot_product_attention(xq, xk, xv, dropout_p=self.dropout if self.training else 0.0, is_causal=self.is_causal)
|
| 46 |
+
else:
|
| 47 |
+
scores = (xq @ xk.transpose(-2, -1)) / math.sqrt(self.head_dim)
|
| 48 |
+
if self.is_causal:
|
| 49 |
+
scores[:, :, :, -seq_len:] += torch.full((seq_len, seq_len), float("-inf"), device=scores.device).triu(1)
|
| 50 |
+
if attention_mask is not None:
|
| 51 |
+
scores += (1.0 - attention_mask.unsqueeze(1).unsqueeze(2)) * -1e9
|
| 52 |
+
output = self.attn_dropout(F.softmax(scores.float(), dim=-1).type_as(xq)) @ xv
|
| 53 |
+
output = output.transpose(1, 2).reshape(bsz, seq_len, -1)
|
| 54 |
+
output = self.resid_dropout(self.o_proj(output))
|
| 55 |
+
return output, past_kv
|
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
|
| 3 |
+
from omni.core.norm import RMSNorm
|
| 4 |
+
from omni.core.attention import Attention
|
| 5 |
+
from omni.core.mlp import FeedForward, MOEFeedForward
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class MiniMindBlock(nn.Module):
|
| 9 |
+
def __init__(self, layer_id: int, config: "MiniMindConfig"):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.self_attn = Attention(config)
|
| 12 |
+
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 13 |
+
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 14 |
+
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config)
|
| 15 |
+
|
| 16 |
+
def forward(self, hidden_states, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None):
|
| 17 |
+
residual = hidden_states
|
| 18 |
+
hidden_states, present_key_value = self.self_attn(
|
| 19 |
+
self.input_layernorm(hidden_states), position_embeddings,
|
| 20 |
+
past_key_value, use_cache, attention_mask
|
| 21 |
+
)
|
| 22 |
+
hidden_states += residual
|
| 23 |
+
hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states))
|
| 24 |
+
return hidden_states, present_key_value
|
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
from torch import nn
|
| 4 |
+
from transformers.activations import ACT2FN
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class FeedForward(nn.Module):
|
| 8 |
+
def __init__(self, config: "MiniMindConfig", intermediate_size: int = None):
|
| 9 |
+
super().__init__()
|
| 10 |
+
intermediate_size = intermediate_size or config.intermediate_size
|
| 11 |
+
self.gate_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
| 12 |
+
self.down_proj = nn.Linear(intermediate_size, config.hidden_size, bias=False)
|
| 13 |
+
self.up_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
| 14 |
+
self.act_fn = ACT2FN[config.hidden_act]
|
| 15 |
+
|
| 16 |
+
def forward(self, x):
|
| 17 |
+
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class MOEFeedForward(nn.Module):
|
| 21 |
+
def __init__(self, config: "MiniMindConfig"):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.config = config
|
| 24 |
+
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
|
| 25 |
+
self.experts = nn.ModuleList([FeedForward(config, intermediate_size=config.moe_intermediate_size) for _ in range(config.num_experts)])
|
| 26 |
+
self.act_fn = ACT2FN[config.hidden_act]
|
| 27 |
+
|
| 28 |
+
def forward(self, x):
|
| 29 |
+
batch_size, seq_len, hidden_dim = x.shape
|
| 30 |
+
x_flat = x.view(-1, hidden_dim)
|
| 31 |
+
scores = F.softmax(self.gate(x_flat), dim=-1)
|
| 32 |
+
topk_weight, topk_idx = torch.topk(scores, k=self.config.num_experts_per_tok, dim=-1, sorted=False)
|
| 33 |
+
if self.config.norm_topk_prob:
|
| 34 |
+
topk_weight = topk_weight / (topk_weight.sum(dim=-1, keepdim=True) + 1e-20)
|
| 35 |
+
y = torch.zeros_like(x_flat)
|
| 36 |
+
for i, expert in enumerate(self.experts):
|
| 37 |
+
mask = (topk_idx == i)
|
| 38 |
+
if mask.any():
|
| 39 |
+
token_idx = mask.any(dim=-1).nonzero().flatten()
|
| 40 |
+
weight = topk_weight[mask].view(-1, 1)
|
| 41 |
+
y.index_add_(0, token_idx, (expert(x_flat[token_idx]) * weight).to(y.dtype))
|
| 42 |
+
elif self.training:
|
| 43 |
+
y[0, 0] += 0 * sum(p.sum() for p in expert.parameters())
|
| 44 |
+
if self.training and self.config.router_aux_loss_coef > 0:
|
| 45 |
+
load = F.one_hot(topk_idx, self.config.num_experts).float().mean(0)
|
| 46 |
+
self.aux_loss = (load * scores.mean(0)).sum() * self.config.num_experts * self.config.router_aux_loss_coef
|
| 47 |
+
else:
|
| 48 |
+
self.aux_loss = scores.new_zeros(1).squeeze()
|
| 49 |
+
return y.view(batch_size, seq_len, hidden_dim)
|
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
|
| 3 |
+
from omni.core.norm import RMSNorm
|
| 4 |
+
from omni.core.rope import precompute_freqs_cis
|
| 5 |
+
from omni.core.block import MiniMindBlock
|
| 6 |
+
from omni.core.mlp import MOEFeedForward
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class MiniMindModel(nn.Module):
|
| 10 |
+
def __init__(self, config: "MiniMindConfig"):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.config = config
|
| 13 |
+
self.vocab_size, self.num_hidden_layers = config.vocab_size, config.num_hidden_layers
|
| 14 |
+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
| 15 |
+
self.dropout = nn.Dropout(config.dropout)
|
| 16 |
+
self.layers = nn.ModuleList([MiniMindBlock(l, config) for l in range(self.num_hidden_layers)])
|
| 17 |
+
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 18 |
+
freqs_cos, freqs_sin = precompute_freqs_cis(dim=config.head_dim, end=config.max_position_embeddings, rope_base=config.rope_theta, rope_scaling=config.rope_scaling)
|
| 19 |
+
self.register_buffer("freqs_cos", freqs_cos, persistent=False)
|
| 20 |
+
self.register_buffer("freqs_sin", freqs_sin, persistent=False)
|
| 21 |
+
|
| 22 |
+
def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, **kwargs):
|
| 23 |
+
batch_size, seq_length = input_ids.shape
|
| 24 |
+
if hasattr(past_key_values, 'layers'):
|
| 25 |
+
past_key_values = None
|
| 26 |
+
past_key_values = past_key_values or [None] * len(self.layers)
|
| 27 |
+
start_pos = past_key_values[0][0].shape[1] if past_key_values[0] is not None else 0
|
| 28 |
+
hidden_states = self.dropout(self.embed_tokens(input_ids))
|
| 29 |
+
if self.freqs_cos[0, 0] == 0:
|
| 30 |
+
freqs_cos, freqs_sin = precompute_freqs_cis(dim=self.config.head_dim, end=self.config.max_position_embeddings, rope_base=self.config.rope_theta, rope_scaling=self.config.rope_scaling)
|
| 31 |
+
self.freqs_cos, self.freqs_sin = freqs_cos.to(hidden_states.device), freqs_sin.to(hidden_states.device)
|
| 32 |
+
position_embeddings = (self.freqs_cos[start_pos:start_pos + seq_length], self.freqs_sin[start_pos:start_pos + seq_length])
|
| 33 |
+
presents = []
|
| 34 |
+
for layer, past_key_value in zip(self.layers, past_key_values):
|
| 35 |
+
hidden_states, present = layer(
|
| 36 |
+
hidden_states,
|
| 37 |
+
position_embeddings,
|
| 38 |
+
past_key_value=past_key_value,
|
| 39 |
+
use_cache=use_cache,
|
| 40 |
+
attention_mask=attention_mask
|
| 41 |
+
)
|
| 42 |
+
presents.append(present)
|
| 43 |
+
hidden_states = self.norm(hidden_states)
|
| 44 |
+
aux_loss = sum([l.mlp.aux_loss for l in self.layers if isinstance(l.mlp, MOEFeedForward)], hidden_states.new_zeros(1).squeeze())
|
| 45 |
+
return hidden_states, presents, aux_loss
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class RMSNorm(torch.nn.Module):
|
| 6 |
+
def __init__(self, dim: int, eps: float = 1e-5):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.eps = eps
|
| 9 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 10 |
+
|
| 11 |
+
def norm(self, x):
|
| 12 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 13 |
+
|
| 14 |
+
def forward(self, x):
|
| 15 |
+
return (self.weight * self.norm(x.float())).type_as(x)
|
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def precompute_freqs_cis(dim: int, end: int = int(32 * 1024), rope_base: float = 1e6, rope_scaling: dict = None):
|
| 6 |
+
freqs, attn_factor = 1.0 / (rope_base ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)), 1.0
|
| 7 |
+
if rope_scaling is not None: # YaRN
|
| 8 |
+
orig_max, factor, beta_fast, beta_slow, attn_factor = (
|
| 9 |
+
rope_scaling.get("original_max_position_embeddings", 2048), rope_scaling.get("factor", 16),
|
| 10 |
+
rope_scaling.get("beta_fast", 32.0), rope_scaling.get("beta_slow", 1.0), rope_scaling.get("attention_factor", 1.0)
|
| 11 |
+
)
|
| 12 |
+
if end / orig_max > 1.0:
|
| 13 |
+
inv_dim = lambda b: (dim * math.log(orig_max / (b * 2 * math.pi))) / (2 * math.log(rope_base))
|
| 14 |
+
low, high = max(math.floor(inv_dim(beta_fast)), 0), min(math.ceil(inv_dim(beta_slow)), dim // 2 - 1)
|
| 15 |
+
ramp = torch.clamp((torch.arange(dim // 2, device=freqs.device).float() - low) / max(high - low, 0.001), 0, 1)
|
| 16 |
+
freqs = freqs * (1 - ramp + ramp / factor)
|
| 17 |
+
t = torch.arange(end, device=freqs.device)
|
| 18 |
+
freqs = torch.outer(t, freqs).float()
|
| 19 |
+
freqs_cos = torch.cat([torch.cos(freqs), torch.cos(freqs)], dim=-1) * attn_factor
|
| 20 |
+
freqs_sin = torch.cat([torch.sin(freqs), torch.sin(freqs)], dim=-1) * attn_factor
|
| 21 |
+
return freqs_cos, freqs_sin
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1):
|
| 25 |
+
def rotate_half(x):
|
| 26 |
+
return torch.cat((-x[..., x.shape[-1] // 2:], x[..., : x.shape[-1] // 2]), dim=-1)
|
| 27 |
+
|
| 28 |
+
q_embed = ((q * cos.unsqueeze(unsqueeze_dim)) + (rotate_half(q) * sin.unsqueeze(unsqueeze_dim))).to(q.dtype)
|
| 29 |
+
k_embed = ((k * cos.unsqueeze(unsqueeze_dim)) + (rotate_half(k) * sin.unsqueeze(unsqueeze_dim))).to(k.dtype)
|
| 30 |
+
return q_embed, k_embed
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
|
| 34 |
+
bs, slen, num_key_value_heads, head_dim = x.shape
|
| 35 |
+
if n_rep == 1:
|
| 36 |
+
return x
|
| 37 |
+
return (x[:, :, :, None, :].expand(bs, slen, num_key_value_heads, n_rep, head_dim).reshape(bs, slen, num_key_value_heads * n_rep, head_dim))
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from omni.datasets.lm_dataset import * # noqa: F401,F403
|
|
@@ -0,0 +1,606 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch.utils.data import Dataset, DataLoader
|
| 2 |
+
import torch
|
| 3 |
+
import io
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import random
|
| 7 |
+
from datasets import load_dataset, Features, Sequence, Value
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from datasets import Dataset as HFDataset
|
| 10 |
+
import pyarrow as pa
|
| 11 |
+
import pyarrow.parquet as pq
|
| 12 |
+
|
| 13 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 14 |
+
|
| 15 |
+
from omni.models import MiniMindVLM
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def pre_processing_chat(conversations, add_system_ratio=0.2):
|
| 19 |
+
if any(conv.get('tools') for conv in conversations):
|
| 20 |
+
return conversations
|
| 21 |
+
|
| 22 |
+
SYSTEM_PROMPTS = [
|
| 23 |
+
"你是一个知识丰富的AI,尽力为用户提供准确的信息。",
|
| 24 |
+
"你是minimind,一个小巧但有用的语言模型。",
|
| 25 |
+
"你是一个专业的AI助手,请提供有价值的回答。",
|
| 26 |
+
"你是minimind,请尽力帮助用户解决问题。",
|
| 27 |
+
"你是一个可靠的AI,请给出准确的回答。",
|
| 28 |
+
"You are a helpful AI assistant.",
|
| 29 |
+
"You are minimind, a lightweight intelligent assistant.",
|
| 30 |
+
"You are a friendly chatbot. Please answer the user's questions carefully.",
|
| 31 |
+
"You are a knowledgeable AI. Try your best to provide accurate information.",
|
| 32 |
+
"You are minimind, a small but useful language model."
|
| 33 |
+
]
|
| 34 |
+
if conversations[0].get('role') != 'system':
|
| 35 |
+
if random.random() < add_system_ratio:
|
| 36 |
+
return [{'role': 'system', 'content': random.choice(SYSTEM_PROMPTS)}] + conversations
|
| 37 |
+
return conversations
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def post_processing_chat(prompt_content, empty_think_ratio=0.2):
|
| 41 |
+
if '<think>\n\n</think>\n\n' in prompt_content and random.random() > empty_think_ratio:
|
| 42 |
+
prompt_content = prompt_content.replace('<think>\n\n</think>\n\n', '')
|
| 43 |
+
return prompt_content
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class PretrainDataset(Dataset):
|
| 47 |
+
def __init__(self, data_path, tokenizer, max_length=512):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.tokenizer = tokenizer
|
| 50 |
+
self.max_length = max_length
|
| 51 |
+
self.samples = load_dataset('json', data_files=data_path, split='train')
|
| 52 |
+
|
| 53 |
+
def __len__(self):
|
| 54 |
+
return len(self.samples)
|
| 55 |
+
|
| 56 |
+
def __getitem__(self, index):
|
| 57 |
+
sample = self.samples[index]
|
| 58 |
+
tokens = self.tokenizer(str(sample['text']), add_special_tokens=False, max_length=self.max_length - 2, truncation=True).input_ids
|
| 59 |
+
tokens = [self.tokenizer.bos_token_id] + tokens + [self.tokenizer.eos_token_id]
|
| 60 |
+
input_ids = tokens + [self.tokenizer.pad_token_id] * (self.max_length - len(tokens))
|
| 61 |
+
input_ids = torch.tensor(input_ids, dtype=torch.long)
|
| 62 |
+
labels = input_ids.clone()
|
| 63 |
+
labels[input_ids == self.tokenizer.pad_token_id] = -100
|
| 64 |
+
return input_ids, labels
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class SFTDataset(Dataset):
|
| 68 |
+
def __init__(self, jsonl_path, tokenizer, max_length=1024):
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.tokenizer = tokenizer
|
| 71 |
+
self.max_length = max_length
|
| 72 |
+
features = Features({'conversations': [{'role': Value('string'), 'content': Value('string'), 'reasoning_content': Value('string'), 'tools': Value('string'), 'tool_calls': Value('string')}]})
|
| 73 |
+
self.samples = load_dataset('json', data_files=jsonl_path, split='train', features=features)
|
| 74 |
+
self.bos_id = tokenizer(f'{tokenizer.bos_token}assistant\n', add_special_tokens=False).input_ids
|
| 75 |
+
self.eos_id = tokenizer(f'{tokenizer.eos_token}\n', add_special_tokens=False).input_ids
|
| 76 |
+
|
| 77 |
+
def __len__(self):
|
| 78 |
+
return len(self.samples)
|
| 79 |
+
|
| 80 |
+
def create_chat_prompt(self, conversations):
|
| 81 |
+
messages = []
|
| 82 |
+
tools = None
|
| 83 |
+
for message in conversations:
|
| 84 |
+
message = dict(message)
|
| 85 |
+
if message.get("role") == "system" and message.get("tools"):
|
| 86 |
+
tools = json.loads(message["tools"]) if isinstance(message["tools"], str) else message["tools"]
|
| 87 |
+
if message.get("tool_calls") and isinstance(message["tool_calls"], str):
|
| 88 |
+
message["tool_calls"] = json.loads(message["tool_calls"])
|
| 89 |
+
messages.append(message)
|
| 90 |
+
return self.tokenizer.apply_chat_template(
|
| 91 |
+
messages,
|
| 92 |
+
tokenize=False,
|
| 93 |
+
add_generation_prompt=False,
|
| 94 |
+
tools=tools
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
def generate_labels(self, input_ids):
|
| 98 |
+
labels = [-100] * len(input_ids)
|
| 99 |
+
i = 0
|
| 100 |
+
while i < len(input_ids):
|
| 101 |
+
if input_ids[i:i + len(self.bos_id)] == self.bos_id:
|
| 102 |
+
start = i + len(self.bos_id)
|
| 103 |
+
end = start
|
| 104 |
+
while end < len(input_ids):
|
| 105 |
+
if input_ids[end:end + len(self.eos_id)] == self.eos_id:
|
| 106 |
+
break
|
| 107 |
+
end += 1
|
| 108 |
+
for j in range(start, min(end + len(self.eos_id), self.max_length)):
|
| 109 |
+
labels[j] = input_ids[j]
|
| 110 |
+
i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids)
|
| 111 |
+
else:
|
| 112 |
+
i += 1
|
| 113 |
+
return labels
|
| 114 |
+
|
| 115 |
+
def __getitem__(self, index):
|
| 116 |
+
sample = self.samples[index]
|
| 117 |
+
conversations = pre_processing_chat(sample['conversations'])
|
| 118 |
+
prompt = self.create_chat_prompt(conversations)
|
| 119 |
+
prompt = post_processing_chat(prompt)
|
| 120 |
+
input_ids = self.tokenizer(prompt).input_ids[:self.max_length]
|
| 121 |
+
input_ids += [self.tokenizer.pad_token_id] * (self.max_length - len(input_ids))
|
| 122 |
+
labels = self.generate_labels(input_ids)
|
| 123 |
+
return torch.tensor(input_ids, dtype=torch.long), torch.tensor(labels, dtype=torch.long)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class DPODataset(Dataset):
|
| 127 |
+
def __init__(self, file_path, tokenizer, max_length=4096):
|
| 128 |
+
super().__init__()
|
| 129 |
+
self.tokenizer = tokenizer
|
| 130 |
+
self.max_length = max_length
|
| 131 |
+
self.padding = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0
|
| 132 |
+
self.bos_id = tokenizer(f'{tokenizer.bos_token}assistant\n', add_special_tokens=False).input_ids
|
| 133 |
+
self.eos_id = tokenizer(f'{tokenizer.eos_token}\n', add_special_tokens=False).input_ids
|
| 134 |
+
self.samples = load_dataset('json', data_files=file_path, split='train')
|
| 135 |
+
|
| 136 |
+
def __len__(self):
|
| 137 |
+
return len(self.samples)
|
| 138 |
+
|
| 139 |
+
def __getitem__(self, index):
|
| 140 |
+
sample = self.samples[index]
|
| 141 |
+
chosen = sample['chosen']
|
| 142 |
+
rejected = sample['rejected']
|
| 143 |
+
chosen_prompt = self.tokenizer.apply_chat_template(
|
| 144 |
+
chosen, tokenize=False, add_generation_prompt=False
|
| 145 |
+
)
|
| 146 |
+
chosen_prompt = post_processing_chat(chosen_prompt)
|
| 147 |
+
|
| 148 |
+
rejected_prompt = self.tokenizer.apply_chat_template(
|
| 149 |
+
rejected, tokenize=False, add_generation_prompt=False
|
| 150 |
+
)
|
| 151 |
+
rejected_prompt = post_processing_chat(rejected_prompt)
|
| 152 |
+
chosen_encoding = self.tokenizer(
|
| 153 |
+
chosen_prompt, truncation=True, max_length=self.max_length, padding='max_length'
|
| 154 |
+
)
|
| 155 |
+
rejected_encoding = self.tokenizer(
|
| 156 |
+
rejected_prompt, truncation=True, max_length=self.max_length, padding='max_length'
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
chosen_input_ids = chosen_encoding['input_ids']
|
| 160 |
+
chosen_loss_mask = self.generate_loss_mask(chosen_input_ids)
|
| 161 |
+
|
| 162 |
+
rejected_input_ids = rejected_encoding['input_ids']
|
| 163 |
+
rejected_loss_mask = self.generate_loss_mask(rejected_input_ids)
|
| 164 |
+
x_chosen = torch.tensor(chosen_input_ids[:-1], dtype=torch.long)
|
| 165 |
+
y_chosen = torch.tensor(chosen_input_ids[1:], dtype=torch.long)
|
| 166 |
+
mask_chosen = torch.tensor(chosen_loss_mask[1:], dtype=torch.long)
|
| 167 |
+
x_rejected = torch.tensor(rejected_input_ids[:-1], dtype=torch.long)
|
| 168 |
+
y_rejected = torch.tensor(rejected_input_ids[1:], dtype=torch.long)
|
| 169 |
+
mask_rejected = torch.tensor(rejected_loss_mask[1:], dtype=torch.long)
|
| 170 |
+
|
| 171 |
+
return {
|
| 172 |
+
'x_chosen': x_chosen,
|
| 173 |
+
'y_chosen': y_chosen,
|
| 174 |
+
'mask_chosen': mask_chosen,
|
| 175 |
+
'x_rejected': x_rejected,
|
| 176 |
+
'y_rejected': y_rejected,
|
| 177 |
+
'mask_rejected': mask_rejected
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
def generate_loss_mask(self, input_ids):
|
| 181 |
+
loss_mask = [0] * len(input_ids)
|
| 182 |
+
i = 0
|
| 183 |
+
while i < len(input_ids):
|
| 184 |
+
if input_ids[i:i + len(self.bos_id)] == self.bos_id:
|
| 185 |
+
start = i + len(self.bos_id)
|
| 186 |
+
end = start
|
| 187 |
+
while end < len(input_ids):
|
| 188 |
+
if input_ids[end:end + len(self.eos_id)] == self.eos_id:
|
| 189 |
+
break
|
| 190 |
+
end += 1
|
| 191 |
+
for j in range(start, min(end + len(self.eos_id), self.max_length)):
|
| 192 |
+
loss_mask[j] = 1
|
| 193 |
+
i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids)
|
| 194 |
+
else:
|
| 195 |
+
i += 1
|
| 196 |
+
return loss_mask
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class RLAIFDataset(Dataset):
|
| 200 |
+
def __init__(self, jsonl_path, tokenizer, max_length=1024, thinking_ratio=0.5):
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.tokenizer = tokenizer
|
| 203 |
+
self.max_length = max_length
|
| 204 |
+
self.thinking_ratio = thinking_ratio
|
| 205 |
+
self.samples = load_dataset('json', data_files=jsonl_path, split='train')
|
| 206 |
+
self.bos_id = tokenizer(f'{tokenizer.bos_token}assistant', add_special_tokens=False).input_ids
|
| 207 |
+
self.eos_id = tokenizer(f'{tokenizer.eos_token}', add_special_tokens=False).input_ids
|
| 208 |
+
|
| 209 |
+
def __len__(self):
|
| 210 |
+
return len(self.samples)
|
| 211 |
+
|
| 212 |
+
def create_chat_prompt(self, conversations):
|
| 213 |
+
conversations = pre_processing_chat(conversations)
|
| 214 |
+
use_thinking = random.random() < self.thinking_ratio
|
| 215 |
+
return self.tokenizer.apply_chat_template(
|
| 216 |
+
conversations[:-1],
|
| 217 |
+
tokenize=False,
|
| 218 |
+
open_thinking=use_thinking,
|
| 219 |
+
add_generation_prompt=True
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
def __getitem__(self, index):
|
| 223 |
+
sample = self.samples[index]
|
| 224 |
+
prompt = self.create_chat_prompt(sample['conversations'])
|
| 225 |
+
|
| 226 |
+
return {
|
| 227 |
+
'prompt': prompt,
|
| 228 |
+
'answer': ""
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class AgentRLDataset(Dataset):
|
| 233 |
+
def __init__(self, jsonl_path, tokenizer, max_length=1024):
|
| 234 |
+
super().__init__()
|
| 235 |
+
self.tokenizer = tokenizer
|
| 236 |
+
self.max_length = max_length
|
| 237 |
+
self.samples = []
|
| 238 |
+
with open(jsonl_path, 'r', encoding='utf-8') as f:
|
| 239 |
+
for line in f:
|
| 240 |
+
self.samples.append(json.loads(line.strip()))
|
| 241 |
+
|
| 242 |
+
def __len__(self):
|
| 243 |
+
return len(self.samples)
|
| 244 |
+
|
| 245 |
+
def parse_conversations(self, conversations):
|
| 246 |
+
messages = []
|
| 247 |
+
tools = None
|
| 248 |
+
for message in conversations:
|
| 249 |
+
message = dict(message)
|
| 250 |
+
if message.get("role") == "system" and message.get("tools"):
|
| 251 |
+
tools = json.loads(message["tools"]) if isinstance(message["tools"], str) else message["tools"]
|
| 252 |
+
messages.append(message)
|
| 253 |
+
return messages[:-1], tools
|
| 254 |
+
|
| 255 |
+
def __getitem__(self, index):
|
| 256 |
+
sample = self.samples[index]
|
| 257 |
+
messages, tools = self.parse_conversations(sample['conversations'])
|
| 258 |
+
return {'messages': messages, 'tools': tools, 'gt': sample['gt']}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
class VLMDataset(Dataset):
|
| 262 |
+
def __init__(self, parquet_path, tokenizer, preprocess=None, max_length=512, image_special_token='<|image_pad|>', image_token_len=64):
|
| 263 |
+
super().__init__()
|
| 264 |
+
self.dataset = HFDataset.from_parquet(parquet_path)
|
| 265 |
+
self.tokenizer = tokenizer
|
| 266 |
+
self.max_length = max_length
|
| 267 |
+
self.preprocess = preprocess
|
| 268 |
+
self.image_special_token = image_special_token * image_token_len
|
| 269 |
+
self.bos_id = tokenizer(f'{tokenizer.bos_token}assistant\n', add_special_tokens=False).input_ids
|
| 270 |
+
self.eos_id = tokenizer(f'{tokenizer.eos_token}\n', add_special_tokens=False).input_ids
|
| 271 |
+
|
| 272 |
+
def __len__(self):
|
| 273 |
+
return len(self.dataset)
|
| 274 |
+
|
| 275 |
+
def create_chat_prompt(self, conversations):
|
| 276 |
+
messages = []
|
| 277 |
+
for turn in conversations:
|
| 278 |
+
content = turn['content'].replace('<image>', self.image_special_token) if turn.get('role') != 'system' else turn['content']
|
| 279 |
+
messages.append({"role": turn['role'], "content": content})
|
| 280 |
+
tools = conversations[0]["functions"] if (conversations and conversations[0]["role"] == "system" and conversations[0].get("functions")) else None
|
| 281 |
+
return self.tokenizer.apply_chat_template(
|
| 282 |
+
messages,
|
| 283 |
+
tokenize=False,
|
| 284 |
+
add_generation_prompt=False,
|
| 285 |
+
tools=tools
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
def generate_labels(self, input_ids):
|
| 289 |
+
labels = [-100] * len(input_ids)
|
| 290 |
+
i = 0
|
| 291 |
+
while i < len(input_ids):
|
| 292 |
+
if input_ids[i:i + len(self.bos_id)] == self.bos_id:
|
| 293 |
+
start = i + len(self.bos_id)
|
| 294 |
+
end = start
|
| 295 |
+
while end < len(input_ids):
|
| 296 |
+
if input_ids[end:end + len(self.eos_id)] == self.eos_id:
|
| 297 |
+
break
|
| 298 |
+
end += 1
|
| 299 |
+
for j in range(start, min(end + len(self.eos_id), self.max_length)):
|
| 300 |
+
labels[j] = input_ids[j]
|
| 301 |
+
i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids)
|
| 302 |
+
else:
|
| 303 |
+
i += 1
|
| 304 |
+
return labels
|
| 305 |
+
|
| 306 |
+
def __getitem__(self, index: int):
|
| 307 |
+
row = self.dataset[index]
|
| 308 |
+
conversations = json.loads(row['conversations']) if isinstance(row['conversations'], str) else row['conversations']
|
| 309 |
+
image_bytes = row['image_bytes']
|
| 310 |
+
if not isinstance(image_bytes, list): image_bytes = [image_bytes]
|
| 311 |
+
|
| 312 |
+
conversations = pre_processing_chat(conversations)
|
| 313 |
+
prompt = self.create_chat_prompt(conversations)
|
| 314 |
+
prompt = post_processing_chat(prompt)
|
| 315 |
+
input_ids = self.tokenizer(prompt).input_ids[:self.max_length]
|
| 316 |
+
input_ids += [self.tokenizer.pad_token_id] * (self.max_length - len(input_ids))
|
| 317 |
+
labels = self.generate_labels(input_ids)
|
| 318 |
+
|
| 319 |
+
image_inputs_list = [MiniMindVLM.image2tensor(Image.open(io.BytesIO(img)), self.preprocess) for img in image_bytes]
|
| 320 |
+
if hasattr(image_inputs_list[0], 'keys'):
|
| 321 |
+
image_data = {k: torch.cat([inp[k] for inp in image_inputs_list], dim=0) for k in image_inputs_list[0].keys()}
|
| 322 |
+
else:
|
| 323 |
+
image_data = torch.stack(image_inputs_list)
|
| 324 |
+
|
| 325 |
+
return torch.tensor(input_ids, dtype=torch.long), torch.tensor(labels, dtype=torch.long), image_data
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class OmniDataset(Dataset):
|
| 329 |
+
def __init__(self, data_path, tokenizer, audio_processor=None, vision_processor=None,
|
| 330 |
+
max_length=1200, audio_special_token='<|audio_pad|>', image_special_token='<|image_pad|>',
|
| 331 |
+
audio_stop_token=2050, # <|audio_stop|>
|
| 332 |
+
audio_pad_token=2049, # <|audio_pad|>
|
| 333 |
+
audio_spk_token=2051, # <|audio_spk|>
|
| 334 |
+
audio_vocab_size=2112, # 2048 mimi codes + 64 special tokens
|
| 335 |
+
scheduled_sampling=0.05,
|
| 336 |
+
image_token_len=64):
|
| 337 |
+
super().__init__()
|
| 338 |
+
import pyarrow as pa
|
| 339 |
+
import pyarrow.parquet as pq
|
| 340 |
+
tables = [pa.Table.from_batches(pq.ParquetFile(p.strip()).iter_batches()) for p in data_path.split(',')]
|
| 341 |
+
tables = [t.cast(pa.schema([f.with_type(pa.large_string()) if pa.types.is_string(f.type) else f for f in t.schema])) for t in tables]
|
| 342 |
+
self.table = pa.concat_tables(tables, promote_options='default')
|
| 343 |
+
self.tokenizer = tokenizer
|
| 344 |
+
self.audio_processor = audio_processor
|
| 345 |
+
self.vision_processor = vision_processor
|
| 346 |
+
self.max_length = max_length
|
| 347 |
+
self.audio_token = audio_special_token
|
| 348 |
+
self.image_token_len = image_token_len
|
| 349 |
+
self.image_token = image_special_token * image_token_len
|
| 350 |
+
self.audio_stop_token = audio_stop_token
|
| 351 |
+
self.audio_pad_token = audio_pad_token
|
| 352 |
+
self.audio_spk_token = audio_spk_token
|
| 353 |
+
self.audio_vocab_size = audio_vocab_size
|
| 354 |
+
self.scheduled_sampling_prob = scheduled_sampling
|
| 355 |
+
self.text_vocab_size = len(tokenizer)
|
| 356 |
+
self.image_token_id = tokenizer.encode(image_special_token, add_special_tokens=False)[0]
|
| 357 |
+
self.audio_token_id = tokenizer.encode(audio_special_token, add_special_tokens=False)[0]
|
| 358 |
+
self.think_end_ids = tokenizer.encode('</think>\n\n', add_special_tokens=False)
|
| 359 |
+
self.bos_id = tokenizer(f'{tokenizer.bos_token}assistant\n', add_special_tokens=False).input_ids
|
| 360 |
+
self.eos_id = tokenizer(f'{tokenizer.eos_token}\n', add_special_tokens=False).input_ids
|
| 361 |
+
|
| 362 |
+
def __len__(self):
|
| 363 |
+
return len(self.table)
|
| 364 |
+
|
| 365 |
+
@staticmethod
|
| 366 |
+
def process_audio(audio_path, audio_processor):
|
| 367 |
+
import soundfile as sf
|
| 368 |
+
import numpy as np
|
| 369 |
+
wav, sr = sf.read(audio_path)
|
| 370 |
+
if wav.ndim > 1: wav = wav.mean(axis=1)
|
| 371 |
+
if sr != 16000:
|
| 372 |
+
import librosa
|
| 373 |
+
wav = librosa.resample(wav.astype(float), orig_sr=sr, target_sr=16000)
|
| 374 |
+
inputs = audio_processor(wav.astype(np.float32), sampling_rate=16000, return_tensors="pt", return_attention_mask=True)
|
| 375 |
+
valid_len = inputs.attention_mask.sum().item()
|
| 376 |
+
return inputs.input_features.squeeze(0), valid_len
|
| 377 |
+
|
| 378 |
+
def augment_wav(self, wav, sr=16000):
|
| 379 |
+
import numpy as np
|
| 380 |
+
from scipy.signal import resample
|
| 381 |
+
if random.random() < 0.5:
|
| 382 |
+
speed = random.uniform(0.7, 1.6)
|
| 383 |
+
wav = resample(wav, int(len(wav) / speed)).astype(np.float32)
|
| 384 |
+
if random.random() < 0.3:
|
| 385 |
+
noise = np.random.randn(len(wav)).astype(np.float32) * random.uniform(0.001, 0.01)
|
| 386 |
+
wav = wav + noise
|
| 387 |
+
if random.random() < 0.3:
|
| 388 |
+
wav = wav * random.uniform(0.8, 1.2)
|
| 389 |
+
if random.random() < 0.2 and len(wav) > sr:
|
| 390 |
+
start = random.randint(0, len(wav) - sr // 4)
|
| 391 |
+
wav[start:start + sr // 4] = 0
|
| 392 |
+
if random.random() < 0.2:
|
| 393 |
+
k = random.choice([3, 5, 7])
|
| 394 |
+
wav = np.convolve(wav, np.ones(k) / k, mode='same').astype(np.float32)
|
| 395 |
+
if random.random() < 0.3:
|
| 396 |
+
ir_len = int(sr * random.uniform(0.05, 0.2))
|
| 397 |
+
ir = np.random.randn(ir_len).astype(np.float32) * np.exp(-np.linspace(0, 10, ir_len))
|
| 398 |
+
ir[0] = 1.0
|
| 399 |
+
ir /= np.sqrt(np.sum(ir ** 2) + 1e-6)
|
| 400 |
+
wav = np.convolve(wav, ir, mode='same').astype(np.float32)
|
| 401 |
+
if random.random() < 0.2:
|
| 402 |
+
pink = np.cumsum(np.random.randn(len(wav))).astype(np.float32)
|
| 403 |
+
pink /= np.max(np.abs(pink)) + 1e-6
|
| 404 |
+
wav = wav + pink * random.uniform(0.003, 0.015)
|
| 405 |
+
return np.clip(wav, -1.0, 1.0).astype(np.float32)
|
| 406 |
+
|
| 407 |
+
def augment_mel(self, fbank):
|
| 408 |
+
import numpy as np
|
| 409 |
+
T, D = fbank.shape
|
| 410 |
+
if random.random() < 0.5:
|
| 411 |
+
f = random.randint(1, 64)
|
| 412 |
+
f0 = random.randint(0, D - f)
|
| 413 |
+
fbank[:, f0:f0 + f] = 0
|
| 414 |
+
if random.random() < 0.5 and T > 1:
|
| 415 |
+
t = random.randint(1, min(10, T))
|
| 416 |
+
t0 = random.randint(0, T - t)
|
| 417 |
+
fbank[t0:t0 + t, :] = 0
|
| 418 |
+
return fbank
|
| 419 |
+
|
| 420 |
+
def load_audio_inputs(self, audio_bytes):
|
| 421 |
+
import soundfile as sf
|
| 422 |
+
import numpy as np
|
| 423 |
+
import io
|
| 424 |
+
import librosa
|
| 425 |
+
if not audio_bytes: return None, 0
|
| 426 |
+
wav, sr = sf.read(io.BytesIO(audio_bytes))
|
| 427 |
+
if wav.ndim > 1: wav = wav.mean(axis=1)
|
| 428 |
+
if sr != 16000: wav = librosa.resample(wav.astype(float), orig_sr=sr, target_sr=16000)
|
| 429 |
+
wav = self.augment_wav(wav.astype(np.float32))
|
| 430 |
+
inputs = self.audio_processor(wav, sampling_rate=16000, return_tensors="pt", return_attention_mask=True)
|
| 431 |
+
valid_len = inputs.attention_mask.sum().item()
|
| 432 |
+
return self.augment_mel(inputs.input_features.squeeze(0)), valid_len
|
| 433 |
+
|
| 434 |
+
def load_image_inputs(self, image_bytes):
|
| 435 |
+
import io
|
| 436 |
+
from PIL import Image
|
| 437 |
+
if not image_bytes or self.vision_processor is None: return None
|
| 438 |
+
image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
|
| 439 |
+
inputs = self.vision_processor(images=image, return_tensors="pt")
|
| 440 |
+
if hasattr(inputs, 'keys'): return {k: v for k, v in inputs.items()}
|
| 441 |
+
return inputs.pixel_values
|
| 442 |
+
|
| 443 |
+
def create_chat_prompt(self, conversations, audio_features_length=0):
|
| 444 |
+
conversations = pre_processing_chat(conversations)
|
| 445 |
+
messages = []
|
| 446 |
+
is_last_user = lambda i: i == max(j for j, t in enumerate(conversations) if t['role'] == 'user')
|
| 447 |
+
for idx, turn in enumerate(conversations):
|
| 448 |
+
role, content = turn['role'], turn['content']
|
| 449 |
+
if role == 'user' and is_last_user(idx) and audio_features_length > 0:
|
| 450 |
+
ap = self.audio_token * audio_features_length
|
| 451 |
+
r = random.random()
|
| 452 |
+
if r < 0.4: content = ap
|
| 453 |
+
elif r < 0.6: content = content
|
| 454 |
+
elif r < 0.8: content = ap + '\n\n' + content
|
| 455 |
+
else: content = content + '\n\n' + ap
|
| 456 |
+
if '<image>' in content:
|
| 457 |
+
r = random.random()
|
| 458 |
+
if r < 0.2: content = '<image>\n' + content.replace('<image>', '').strip()
|
| 459 |
+
elif r < 0.4: content = '<image>\n\n' + content.replace('<image>', '').strip()
|
| 460 |
+
elif r < 0.6: content = content.replace('<image>', '').strip() + '\n' + '<image>'
|
| 461 |
+
else: content = content.replace('<image>', '').strip() + '\n\n' + '<image>'
|
| 462 |
+
messages.append({"role": role, "content": content})
|
| 463 |
+
prompt = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
|
| 464 |
+
return post_processing_chat(prompt)
|
| 465 |
+
|
| 466 |
+
def generate_text_labels(self, input_ids):
|
| 467 |
+
labels = [-100] * len(input_ids)
|
| 468 |
+
ranges = []
|
| 469 |
+
i = 0
|
| 470 |
+
while i < len(input_ids):
|
| 471 |
+
if input_ids[i:i + len(self.bos_id)] == self.bos_id:
|
| 472 |
+
start = i + len(self.bos_id)
|
| 473 |
+
end = start
|
| 474 |
+
while end < len(input_ids):
|
| 475 |
+
if input_ids[end:end + len(self.eos_id)] == self.eos_id:
|
| 476 |
+
break
|
| 477 |
+
end += 1
|
| 478 |
+
ranges.append((start, end))
|
| 479 |
+
for j in range(start, min(end + len(self.eos_id), self.max_length)):
|
| 480 |
+
labels[j] = input_ids[j]
|
| 481 |
+
i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids)
|
| 482 |
+
else:
|
| 483 |
+
i += 1
|
| 484 |
+
return labels, ranges
|
| 485 |
+
|
| 486 |
+
def apply_scheduled_sampling(self, input_ids, audio_labels, text_labels):
|
| 487 |
+
if self.scheduled_sampling_prob <= 0:
|
| 488 |
+
return input_ids
|
| 489 |
+
audio_mask = (audio_labels != -100).any(dim=0) & (torch.rand(input_ids.size(1)) < self.scheduled_sampling_prob)
|
| 490 |
+
for i in range(8):
|
| 491 |
+
input_ids[i] = torch.where(audio_mask, torch.randint(0, self.audio_vocab_size, input_ids[i].shape), input_ids[i])
|
| 492 |
+
text_mask = (text_labels != -100) & (input_ids[8] != self.image_token_id) & (torch.rand(input_ids.size(1)) < self.scheduled_sampling_prob)
|
| 493 |
+
input_ids[8] = torch.where(text_mask, torch.randint(0, self.text_vocab_size, input_ids[8].shape), input_ids[8])
|
| 494 |
+
return input_ids
|
| 495 |
+
|
| 496 |
+
def __getitem__(self, index: int):
|
| 497 |
+
import numpy as np
|
| 498 |
+
conversations = json.loads(self.table['conversations'][index].as_py())
|
| 499 |
+
question_audios = self.table['question_audios'][index].as_py() if 'question_audios' in self.table.column_names else []
|
| 500 |
+
answer_audios = self.table['answer_audios'][index].as_py() if 'answer_audios' in self.table.column_names else []
|
| 501 |
+
image_bytes = self.table['image_bytes'][index].as_py() if 'image_bytes' in self.table.column_names else []
|
| 502 |
+
if image_bytes and not isinstance(image_bytes, list): image_bytes = [image_bytes]
|
| 503 |
+
ref_audios = self.table['ref_audios'][index].as_py() if 'ref_audios' in self.table.column_names else []
|
| 504 |
+
spk_emb_raw = self.table['spk_emb'][index].as_py() if 'spk_emb' in self.table.column_names else []
|
| 505 |
+
|
| 506 |
+
asst_indices = [i for i, t in enumerate(conversations) if t['role'] == 'assistant']
|
| 507 |
+
if len(asst_indices) > 1:
|
| 508 |
+
rand_idx = random.randint(0, len(asst_indices) - 1)
|
| 509 |
+
for i in range(rand_idx, -1, -1):
|
| 510 |
+
conversations = conversations[:asst_indices[i] + 1]
|
| 511 |
+
test_prompt = self.create_chat_prompt(conversations, 0)
|
| 512 |
+
if len(self.tokenizer(test_prompt).input_ids) + 100 < self.max_length:
|
| 513 |
+
break
|
| 514 |
+
|
| 515 |
+
pixel_values = None
|
| 516 |
+
user_count = sum(1 for t in conversations if t['role'] == 'user')
|
| 517 |
+
if image_bytes and len(image_bytes) > 0 and self.vision_processor:
|
| 518 |
+
pixel_values = self.load_image_inputs(image_bytes[0])
|
| 519 |
+
|
| 520 |
+
audio_inputs, audio_len, audio_features_length = None, 0, 0
|
| 521 |
+
user_count = sum(1 for t in conversations if t['role'] == 'user')
|
| 522 |
+
if question_audios and user_count > 0 and user_count <= len(question_audios) and self.audio_processor:
|
| 523 |
+
audio_bytes = question_audios[user_count - 1]
|
| 524 |
+
if audio_bytes:
|
| 525 |
+
mel, valid_len = self.load_audio_inputs(audio_bytes)
|
| 526 |
+
if mel is not None:
|
| 527 |
+
audio_inputs = mel.unsqueeze(0)
|
| 528 |
+
audio_len = valid_len
|
| 529 |
+
audio_features_length = valid_len or 1
|
| 530 |
+
|
| 531 |
+
if audio_inputs is None and self.audio_processor:
|
| 532 |
+
audio_inputs = torch.zeros(1, 1, 560)
|
| 533 |
+
audio_len = 0
|
| 534 |
+
if pixel_values is None and self.vision_processor:
|
| 535 |
+
pixel_values = {'pixel_values': torch.zeros(1, 3, 256, 256)}
|
| 536 |
+
|
| 537 |
+
last_audio_codes = None
|
| 538 |
+
asst_count = sum(1 for t in conversations if t['role'] == 'assistant')
|
| 539 |
+
if answer_audios and asst_count > 0 and asst_count <= len(answer_audios):
|
| 540 |
+
tokens = answer_audios[asst_count - 1]
|
| 541 |
+
if tokens:
|
| 542 |
+
audio_codes_8layers = [[] for _ in range(8)]
|
| 543 |
+
for i in range(0, len(tokens) - 7, 8):
|
| 544 |
+
for j in range(8): audio_codes_8layers[j].append(tokens[i + j])
|
| 545 |
+
for layer in audio_codes_8layers: layer.append(self.audio_stop_token)
|
| 546 |
+
last_audio_codes = audio_codes_8layers
|
| 547 |
+
|
| 548 |
+
prompt = self.create_chat_prompt(conversations, audio_features_length)
|
| 549 |
+
if pixel_values is not None: prompt = prompt.replace('<image>', self.image_token)
|
| 550 |
+
input_ids = self.tokenizer(prompt).input_ids[:self.max_length]
|
| 551 |
+
|
| 552 |
+
input_ids += [self.tokenizer.pad_token_id] * (self.max_length - len(input_ids))
|
| 553 |
+
|
| 554 |
+
text_labels, assistant_ranges = self.generate_text_labels(input_ids)
|
| 555 |
+
for start, end in assistant_ranges[:-1]:
|
| 556 |
+
mask_end = min(end + len(self.eos_id), self.max_length)
|
| 557 |
+
text_labels[start:mask_end] = [-100] * (mask_end - start)
|
| 558 |
+
|
| 559 |
+
Y_audio_layers = [[self.audio_pad_token] * self.max_length for _ in range(8)]
|
| 560 |
+
audio_labels = [[-100] * self.max_length for _ in range(8)]
|
| 561 |
+
if assistant_ranges and last_audio_codes:
|
| 562 |
+
assistant_start, assistant_end = assistant_ranges[-1]
|
| 563 |
+
for pos in range(assistant_start, min(assistant_end, assistant_start + 50)):
|
| 564 |
+
if input_ids[pos:pos + len(self.think_end_ids)] == self.think_end_ids:
|
| 565 |
+
assistant_start = pos + len(self.think_end_ids)
|
| 566 |
+
break
|
| 567 |
+
has_spk = bool(spk_emb_raw)
|
| 568 |
+
has_ref = bool(ref_audios) and random.random() > 0.5
|
| 569 |
+
spk_reserve = 1 if has_spk else 0
|
| 570 |
+
if has_ref:
|
| 571 |
+
ref_codes = [[] for _ in range(8)]
|
| 572 |
+
for i in range(0, len(ref_audios) - 7, 8):
|
| 573 |
+
for j in range(8): ref_codes[j].append(ref_audios[i + j])
|
| 574 |
+
ref_len = len(ref_codes[0])
|
| 575 |
+
ref_start = max(spk_reserve, assistant_start - ref_len)
|
| 576 |
+
for layer_idx in range(8):
|
| 577 |
+
codes = ref_codes[layer_idx][-(assistant_start - ref_start):] if ref_len > (assistant_start - ref_start) else ref_codes[layer_idx]
|
| 578 |
+
for i, code in enumerate(codes):
|
| 579 |
+
Y_audio_layers[layer_idx][ref_start + i] = code
|
| 580 |
+
else:
|
| 581 |
+
ref_start = assistant_start
|
| 582 |
+
if has_spk and ref_start > 0:
|
| 583 |
+
spk_pos = ref_start - 1
|
| 584 |
+
for layer_idx in range(8):
|
| 585 |
+
Y_audio_layers[layer_idx][spk_pos] = self.audio_spk_token
|
| 586 |
+
for layer_idx in range(8):
|
| 587 |
+
codes = last_audio_codes[layer_idx]
|
| 588 |
+
start_pos = assistant_start + layer_idx + 1
|
| 589 |
+
for i, code in enumerate(codes):
|
| 590 |
+
if start_pos + i < self.max_length:
|
| 591 |
+
Y_audio_layers[layer_idx][start_pos + i] = code
|
| 592 |
+
audio_labels[layer_idx][start_pos + i] = code
|
| 593 |
+
|
| 594 |
+
X_audio = torch.tensor([layer[:-1] for layer in Y_audio_layers], dtype=torch.long) # (8, T-1)
|
| 595 |
+
X_text = torch.tensor(input_ids[:-1], dtype=torch.long) # (T-1,)
|
| 596 |
+
input_ids = torch.cat((X_audio, X_text.unsqueeze(0)), dim=0) # (9, T-1)
|
| 597 |
+
text_labels = torch.tensor(text_labels[1:], dtype=torch.long) # (T-1,)
|
| 598 |
+
audio_labels = torch.tensor([layer[1:] for layer in audio_labels], dtype=torch.long) # (8, T-1)
|
| 599 |
+
|
| 600 |
+
input_ids = self.apply_scheduled_sampling(input_ids, audio_labels, text_labels)
|
| 601 |
+
spk_emb = torch.tensor(spk_emb_raw, dtype=torch.float32) if spk_emb_raw else torch.zeros(192)
|
| 602 |
+
return input_ids, text_labels, audio_labels, audio_inputs, audio_len, pixel_values, spk_emb
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
if __name__ == "__main__":
|
| 606 |
+
pass
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.encoders.vision import SiglipVisionEncoder
|
| 2 |
+
|
| 3 |
+
__all__ = ["SiglipVisionEncoder"]
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.encoders.audio.sensevoice import SenseVoiceAudioEncoder, SenseVoiceAudioProcessor
|
| 2 |
+
|
| 3 |
+
__all__ = ["SenseVoiceAudioEncoder", "SenseVoiceAudioProcessor"]
|
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
import contextlib
|
| 4 |
+
import io
|
| 5 |
+
import warnings
|
| 6 |
+
import torch
|
| 7 |
+
import numpy as np
|
| 8 |
+
from torch import nn
|
| 9 |
+
from types import SimpleNamespace
|
| 10 |
+
|
| 11 |
+
from transformers import logging as hf_logging
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class SenseVoiceAudioProcessor:
|
| 15 |
+
def __init__(self, frontend):
|
| 16 |
+
self.frontend = frontend
|
| 17 |
+
|
| 18 |
+
def __call__(self, wav, sampling_rate=16000, return_tensors="pt", return_attention_mask=True, **kwargs):
|
| 19 |
+
if isinstance(wav, np.ndarray):
|
| 20 |
+
wav = torch.from_numpy(wav).float()
|
| 21 |
+
if wav.dim() == 1:
|
| 22 |
+
wav = wav.unsqueeze(0)
|
| 23 |
+
with torch.no_grad():
|
| 24 |
+
fbank, flen = self.frontend(wav, torch.tensor([wav.size(1)]))
|
| 25 |
+
return SimpleNamespace(
|
| 26 |
+
input_features=fbank,
|
| 27 |
+
attention_mask=(torch.arange(fbank.size(1)) < flen[0]).long().unsqueeze(0),
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class SenseVoiceAudioEncoder(nn.Module):
|
| 32 |
+
"""Frozen SenseVoice audio encoder (funasr AutoModel). Returns frame embeddings."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, model_path: str = None):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.model = None
|
| 37 |
+
self.processor = None
|
| 38 |
+
if model_path is not None and os.path.exists(model_path):
|
| 39 |
+
self.load(model_path)
|
| 40 |
+
|
| 41 |
+
@torch.no_grad()
|
| 42 |
+
def load(self, model_path: str):
|
| 43 |
+
if not os.path.exists(model_path):
|
| 44 |
+
warnings.warn(f"[SenseVoiceAudioEncoder] path not found: {model_path}")
|
| 45 |
+
return
|
| 46 |
+
logging.getLogger().setLevel(logging.ERROR)
|
| 47 |
+
hf_logging.set_verbosity_error()
|
| 48 |
+
with contextlib.redirect_stdout(io.StringIO()):
|
| 49 |
+
from funasr import AutoModel
|
| 50 |
+
m = AutoModel(model=model_path, trust_remote_code=True, disable_update=True, device="cpu")
|
| 51 |
+
encoder, frontend = m.model.encoder, m.kwargs["frontend"]
|
| 52 |
+
for p in encoder.parameters():
|
| 53 |
+
p.requires_grad = False
|
| 54 |
+
self.model = encoder.eval().float()
|
| 55 |
+
self.processor = SenseVoiceAudioProcessor(frontend.eval())
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def hidden_size(self) -> int:
|
| 59 |
+
if self.model is None:
|
| 60 |
+
return 0
|
| 61 |
+
return self.model.config.d_model if hasattr(self.model.config, "d_model") else self.model.config.hidden_size
|
| 62 |
+
|
| 63 |
+
@torch.no_grad()
|
| 64 |
+
def encode(self, fbank, audio_lens=None):
|
| 65 |
+
if self.model is None:
|
| 66 |
+
return None
|
| 67 |
+
if audio_lens is None:
|
| 68 |
+
audio_lens = torch.tensor([fbank.size(1)] * fbank.size(0), device=fbank.device)
|
| 69 |
+
emb, _ = self.model(fbank, audio_lens)
|
| 70 |
+
return emb
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.encoders.vision.siglip import SiglipVisionEncoder
|
| 2 |
+
|
| 3 |
+
__all__ = ["SiglipVisionEncoder"]
|
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import warnings
|
| 5 |
+
from torch import nn
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
from transformers import SiglipVisionModel, SiglipImageProcessor
|
| 9 |
+
from transformers import logging as hf_logging
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class SiglipVisionEncoder(nn.Module):
|
| 13 |
+
"""Frozen SigLIP vision encoder. Returns last-hidden-state embeddings (B, T, D)."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, model_path: Optional[str] = None):
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.model = None
|
| 18 |
+
self.processor = None
|
| 19 |
+
if model_path is not None and os.path.exists(model_path):
|
| 20 |
+
self.load(model_path)
|
| 21 |
+
|
| 22 |
+
@torch.no_grad()
|
| 23 |
+
def load(self, model_path: str):
|
| 24 |
+
hf_logging.set_verbosity_error()
|
| 25 |
+
try:
|
| 26 |
+
model = SiglipVisionModel.from_pretrained(model_path)
|
| 27 |
+
except (RuntimeError, ValueError):
|
| 28 |
+
warnings.warn(f"[SiglipVisionEncoder] failed to load from {model_path}")
|
| 29 |
+
return
|
| 30 |
+
processor = SiglipImageProcessor.from_pretrained(model_path)
|
| 31 |
+
for param in model.parameters():
|
| 32 |
+
param.requires_grad = False
|
| 33 |
+
self.model = model.eval()
|
| 34 |
+
self.processor = processor
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def hidden_size(self) -> int:
|
| 38 |
+
if self.model is None:
|
| 39 |
+
return 0
|
| 40 |
+
return self.model.config.hidden_size
|
| 41 |
+
|
| 42 |
+
def preprocess(self, image):
|
| 43 |
+
if image.mode in ('RGBA', 'LA'):
|
| 44 |
+
image = image.convert('RGB')
|
| 45 |
+
return self.processor(images=image, return_tensors='pt')
|
| 46 |
+
|
| 47 |
+
@torch.no_grad()
|
| 48 |
+
def encode(self, pixel_values):
|
| 49 |
+
if self.model is None:
|
| 50 |
+
return None
|
| 51 |
+
if hasattr(pixel_values, 'keys'):
|
| 52 |
+
pixel_values = {k: (v.squeeze(1) if v.ndim > 2 and v.shape[1] == 1 else v) for k, v in pixel_values.items()}
|
| 53 |
+
outputs = self.model(**pixel_values)
|
| 54 |
+
return outputs.last_hidden_state
|
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.core import (
|
| 2 |
+
RMSNorm,
|
| 3 |
+
Attention,
|
| 4 |
+
FeedForward,
|
| 5 |
+
MOEFeedForward,
|
| 6 |
+
MiniMindBlock,
|
| 7 |
+
MiniMindModel,
|
| 8 |
+
precompute_freqs_cis,
|
| 9 |
+
apply_rotary_pos_emb,
|
| 10 |
+
)
|
| 11 |
+
from omni.models.minimind import (
|
| 12 |
+
MiniMindConfig,
|
| 13 |
+
MiniMindForCausalLM,
|
| 14 |
+
)
|
| 15 |
+
from omni.models.vlm import (
|
| 16 |
+
VLMConfig,
|
| 17 |
+
MiniMindVLM,
|
| 18 |
+
)
|
| 19 |
+
from omni.models.omni import (
|
| 20 |
+
OmniConfig,
|
| 21 |
+
MiniMindOmni,
|
| 22 |
+
TalkerModule,
|
| 23 |
+
)
|
| 24 |
+
from omni.models.lora import (
|
| 25 |
+
LoRA,
|
| 26 |
+
apply_lora,
|
| 27 |
+
load_lora,
|
| 28 |
+
save_lora,
|
| 29 |
+
merge_lora,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
__all__ = [
|
| 33 |
+
"MiniMindConfig",
|
| 34 |
+
"MiniMindModel",
|
| 35 |
+
"MiniMindForCausalLM",
|
| 36 |
+
"VLMConfig",
|
| 37 |
+
"MiniMindVLM",
|
| 38 |
+
"OmniConfig",
|
| 39 |
+
"MiniMindOmni",
|
| 40 |
+
"TalkerModule",
|
| 41 |
+
"RMSNorm",
|
| 42 |
+
"Attention",
|
| 43 |
+
"FeedForward",
|
| 44 |
+
"MOEFeedForward",
|
| 45 |
+
"MiniMindBlock",
|
| 46 |
+
"precompute_freqs_cis",
|
| 47 |
+
"apply_rotary_pos_emb",
|
| 48 |
+
"LoRA",
|
| 49 |
+
"apply_lora",
|
| 50 |
+
"load_lora",
|
| 51 |
+
"save_lora",
|
| 52 |
+
"merge_lora",
|
| 53 |
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import optim, nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class LoRA(nn.Module):
|
| 6 |
+
def __init__(self, in_features, out_features, rank):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.rank = rank
|
| 9 |
+
self.A = nn.Linear(in_features, rank, bias=False)
|
| 10 |
+
self.B = nn.Linear(rank, out_features, bias=False)
|
| 11 |
+
self.A.weight.data.normal_(mean=0.0, std=0.02)
|
| 12 |
+
self.B.weight.data.zero_()
|
| 13 |
+
|
| 14 |
+
def forward(self, x):
|
| 15 |
+
return self.B(self.A(x))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def apply_lora(model, rank=16):
|
| 19 |
+
for name, module in model.named_modules():
|
| 20 |
+
if isinstance(module, nn.Linear) and module.in_features == module.out_features:
|
| 21 |
+
lora = LoRA(module.in_features, module.out_features, rank=rank).to(model.device)
|
| 22 |
+
setattr(module, "lora", lora)
|
| 23 |
+
original_forward = module.forward
|
| 24 |
+
|
| 25 |
+
def forward_with_lora(x, layer1=original_forward, layer2=lora):
|
| 26 |
+
return layer1(x) + layer2(x)
|
| 27 |
+
|
| 28 |
+
module.forward = forward_with_lora
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_lora(model, path):
|
| 32 |
+
state_dict = torch.load(path, map_location=model.device)
|
| 33 |
+
state_dict = {(k[7:] if k.startswith('module.') else k): v for k, v in state_dict.items()}
|
| 34 |
+
|
| 35 |
+
for name, module in model.named_modules():
|
| 36 |
+
if hasattr(module, 'lora'):
|
| 37 |
+
lora_state = {k.replace(f'{name}.lora.', ''): v for k, v in state_dict.items() if f'{name}.lora.' in k}
|
| 38 |
+
module.lora.load_state_dict(lora_state)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def save_lora(model, path):
|
| 42 |
+
raw_model = getattr(model, '_orig_mod', model)
|
| 43 |
+
state_dict = {}
|
| 44 |
+
for name, module in raw_model.named_modules():
|
| 45 |
+
if hasattr(module, 'lora'):
|
| 46 |
+
clean_name = name[7:] if name.startswith("module.") else name
|
| 47 |
+
lora_state = {f'{clean_name}.lora.{k}': v.cpu().half() for k, v in module.lora.state_dict().items()}
|
| 48 |
+
state_dict.update(lora_state)
|
| 49 |
+
torch.save(state_dict, path)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def merge_lora(model, lora_path, save_path):
|
| 53 |
+
load_lora(model, lora_path)
|
| 54 |
+
raw_model = getattr(model, '_orig_mod', model)
|
| 55 |
+
state_dict = {k: v.cpu().half() for k, v in raw_model.state_dict().items() if '.lora.' not in k}
|
| 56 |
+
for name, module in raw_model.named_modules():
|
| 57 |
+
if isinstance(module, nn.Linear) and '.lora.' not in name:
|
| 58 |
+
state_dict[f'{name}.weight'] = module.weight.data.clone().cpu().half()
|
| 59 |
+
if hasattr(module, 'lora'):
|
| 60 |
+
state_dict[f'{name}.weight'] += (module.lora.B.weight.data @ module.lora.A.weight.data).cpu().half()
|
| 61 |
+
torch.save(state_dict, save_path)
|
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch import nn
|
| 5 |
+
from transformers import PreTrainedModel, GenerationMixin, PretrainedConfig
|
| 6 |
+
from transformers.modeling_outputs import MoeCausalLMOutputWithPast
|
| 7 |
+
|
| 8 |
+
from omni.core import MiniMindModel
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class MiniMindConfig(PretrainedConfig):
|
| 12 |
+
model_type = "minimind"
|
| 13 |
+
|
| 14 |
+
def __init__(self, hidden_size=768, num_hidden_layers=8, use_moe=False, **kwargs):
|
| 15 |
+
super().__init__(**kwargs)
|
| 16 |
+
self.hidden_size = hidden_size
|
| 17 |
+
self.num_hidden_layers = num_hidden_layers
|
| 18 |
+
self.use_moe = use_moe
|
| 19 |
+
self.dropout = kwargs.get("dropout", 0.0)
|
| 20 |
+
self.vocab_size = kwargs.get("vocab_size", 6400)
|
| 21 |
+
self.bos_token_id = kwargs.get("bos_token_id", 1)
|
| 22 |
+
self.eos_token_id = kwargs.get("eos_token_id", 2)
|
| 23 |
+
self.flash_attn = kwargs.get("flash_attn", True)
|
| 24 |
+
self.num_attention_heads = kwargs.get("num_attention_heads", 8)
|
| 25 |
+
self.num_key_value_heads = kwargs.get("num_key_value_heads", 4)
|
| 26 |
+
self.head_dim = kwargs.get("head_dim", self.hidden_size // self.num_attention_heads)
|
| 27 |
+
self.hidden_act = kwargs.get("hidden_act", 'silu')
|
| 28 |
+
self.intermediate_size = kwargs.get("intermediate_size", math.ceil(hidden_size * math.pi / 64) * 64)
|
| 29 |
+
self.max_position_embeddings = kwargs.get("max_position_embeddings", 32768)
|
| 30 |
+
self.rms_norm_eps = kwargs.get("rms_norm_eps", 1e-6)
|
| 31 |
+
self.rope_theta = kwargs.get("rope_theta", 1e6)
|
| 32 |
+
self.tie_word_embeddings = kwargs.get("tie_word_embeddings", True)
|
| 33 |
+
self.inference_rope_scaling = kwargs.get("inference_rope_scaling", False)
|
| 34 |
+
self.rope_scaling = {
|
| 35 |
+
"beta_fast": 32,
|
| 36 |
+
"beta_slow": 1,
|
| 37 |
+
"factor": 16,
|
| 38 |
+
"original_max_position_embeddings": 2048,
|
| 39 |
+
"attention_factor": 1.0,
|
| 40 |
+
"type": "yarn"
|
| 41 |
+
} if self.inference_rope_scaling else None
|
| 42 |
+
# MoE specific configs (ignored if use_moe = False)
|
| 43 |
+
self.num_experts = kwargs.get("num_experts", 4)
|
| 44 |
+
self.num_experts_per_tok = kwargs.get("num_experts_per_tok", 1)
|
| 45 |
+
self.moe_intermediate_size = kwargs.get("moe_intermediate_size", self.intermediate_size)
|
| 46 |
+
self.norm_topk_prob = kwargs.get("norm_topk_prob", True)
|
| 47 |
+
self.router_aux_loss_coef = kwargs.get("router_aux_loss_coef", 5e-4)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class MiniMindForCausalLM(PreTrainedModel, GenerationMixin):
|
| 51 |
+
config_class = MiniMindConfig
|
| 52 |
+
_tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
|
| 53 |
+
|
| 54 |
+
def __init__(self, config: MiniMindConfig = None):
|
| 55 |
+
self.config = config or MiniMindConfig()
|
| 56 |
+
super().__init__(self.config)
|
| 57 |
+
self.model = MiniMindModel(self.config)
|
| 58 |
+
self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
|
| 59 |
+
if self.config.tie_word_embeddings:
|
| 60 |
+
self.model.embed_tokens.weight = self.lm_head.weight
|
| 61 |
+
self.post_init()
|
| 62 |
+
|
| 63 |
+
def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, logits_to_keep=0, labels=None, **kwargs):
|
| 64 |
+
hidden_states, past_key_values, aux_loss = self.model(input_ids, attention_mask, past_key_values, use_cache, **kwargs)
|
| 65 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 66 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 67 |
+
loss = None
|
| 68 |
+
if labels is not None:
|
| 69 |
+
x, y = logits[..., :-1, :].contiguous(), labels[..., 1:].contiguous()
|
| 70 |
+
loss = F.cross_entropy(x.view(-1, x.size(-1)), y.view(-1), ignore_index=-100)
|
| 71 |
+
return MoeCausalLMOutputWithPast(loss=loss, aux_loss=aux_loss, logits=logits, past_key_values=past_key_values, hidden_states=hidden_states)
|
| 72 |
+
|
| 73 |
+
@torch.inference_mode()
|
| 74 |
+
def generate(self, inputs=None, attention_mask=None, max_new_tokens=8192, temperature=0.85, top_p=0.85, top_k=50, eos_token_id=2, streamer=None, use_cache=True, num_return_sequences=1, do_sample=True, repetition_penalty=1.0, **kwargs):
|
| 75 |
+
input_ids = kwargs.pop("input_ids", inputs).repeat(num_return_sequences, 1)
|
| 76 |
+
attention_mask = attention_mask.repeat(num_return_sequences, 1) if attention_mask is not None else None
|
| 77 |
+
past_key_values = kwargs.pop("past_key_values", None)
|
| 78 |
+
finished = torch.zeros(input_ids.shape[0], dtype=torch.bool, device=input_ids.device)
|
| 79 |
+
if streamer:
|
| 80 |
+
streamer.put(input_ids.cpu())
|
| 81 |
+
for _ in range(max_new_tokens):
|
| 82 |
+
past_len = past_key_values[0][0].shape[1] if past_key_values else 0
|
| 83 |
+
outputs = self.forward(input_ids[:, past_len:], attention_mask, past_key_values, use_cache=use_cache, **kwargs)
|
| 84 |
+
attention_mask = torch.cat([attention_mask, attention_mask.new_ones(attention_mask.shape[0], 1)], -1) if attention_mask is not None else None
|
| 85 |
+
logits = outputs.logits[:, -1, :] / temperature
|
| 86 |
+
if repetition_penalty != 1.0:
|
| 87 |
+
for i in range(input_ids.shape[0]):
|
| 88 |
+
seen = torch.unique(input_ids[i])
|
| 89 |
+
score = logits[i, seen]
|
| 90 |
+
logits[i, seen] = torch.where(score > 0, score / repetition_penalty, score * repetition_penalty)
|
| 91 |
+
if top_k > 0:
|
| 92 |
+
logits[logits < torch.topk(logits, top_k)[0][..., -1, None]] = -float('inf')
|
| 93 |
+
if top_p < 1.0:
|
| 94 |
+
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
|
| 95 |
+
mask = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1) > top_p
|
| 96 |
+
mask[..., 1:], mask[..., 0] = mask[..., :-1].clone(), 0
|
| 97 |
+
logits[mask.scatter(1, sorted_indices, mask)] = -float('inf')
|
| 98 |
+
next_token = torch.multinomial(torch.softmax(logits, dim=-1), num_samples=1) if do_sample else torch.argmax(logits, dim=-1, keepdim=True)
|
| 99 |
+
if eos_token_id is not None:
|
| 100 |
+
next_token = torch.where(finished.unsqueeze(-1), next_token.new_full((next_token.shape[0], 1), eos_token_id), next_token)
|
| 101 |
+
input_ids = torch.cat([input_ids, next_token], dim=-1)
|
| 102 |
+
past_key_values = outputs.past_key_values if use_cache else None
|
| 103 |
+
if streamer:
|
| 104 |
+
streamer.put(next_token.cpu())
|
| 105 |
+
if eos_token_id is not None:
|
| 106 |
+
finished |= next_token.squeeze(-1).eq(eos_token_id)
|
| 107 |
+
if finished.all():
|
| 108 |
+
break
|
| 109 |
+
if streamer:
|
| 110 |
+
streamer.end()
|
| 111 |
+
if kwargs.get("return_kv"):
|
| 112 |
+
return {'generated_ids': input_ids, 'past_kv': past_key_values}
|
| 113 |
+
return input_ids
|
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import math
|
| 3 |
+
import torch
|
| 4 |
+
import warnings
|
| 5 |
+
import logging
|
| 6 |
+
import contextlib
|
| 7 |
+
import io
|
| 8 |
+
from torch import nn
|
| 9 |
+
from torch.nn import functional as F
|
| 10 |
+
from transformers.modeling_outputs import MoeCausalLMOutputWithPast
|
| 11 |
+
from transformers import SiglipVisionModel, SiglipImageProcessor, logging as hf_logging
|
| 12 |
+
|
| 13 |
+
from omni.core import RMSNorm, precompute_freqs_cis, apply_rotary_pos_emb, MiniMindBlock, MOEFeedForward
|
| 14 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 15 |
+
from omni.encoders.audio import SenseVoiceAudioEncoder
|
| 16 |
+
from omni.encoders.vision import SiglipVisionEncoder
|
| 17 |
+
from omni.projectors import MMVisionProjector, MMAudioProjector
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class OmniConfig(MiniMindConfig):
|
| 21 |
+
model_type = "minimind-o"
|
| 22 |
+
|
| 23 |
+
def __init__(self, **kwargs):
|
| 24 |
+
super().__init__(**kwargs)
|
| 25 |
+
self.num_talker_hidden_layers = kwargs.get("num_talker_hidden_layers", 4)
|
| 26 |
+
self.talker_hidden_size = kwargs.get("talker_hidden_size", 768)
|
| 27 |
+
self.audio_ids = kwargs.get("audio_ids", [16])
|
| 28 |
+
self.audio_special_token = kwargs.get("audio_special_token", "<|audio_pad|>")
|
| 29 |
+
self.audio_hidden_size = kwargs.get("audio_hidden_size", 512)
|
| 30 |
+
self.audio_vocab_size = kwargs.get("audio_vocab_size", 2112)
|
| 31 |
+
self.audio_pad_token = kwargs.get("audio_pad_token", 2049)
|
| 32 |
+
self.audio_stop_token = kwargs.get("audio_stop_token", 2050)
|
| 33 |
+
self.audio_spk_token = kwargs.get("audio_spk_token", 2051)
|
| 34 |
+
self.spk_emb_size = kwargs.get("spk_emb_size", 192)
|
| 35 |
+
self.think_end_ids = kwargs.get("think_end_ids", [26, 234, 234])
|
| 36 |
+
self.image_ids = kwargs.get("image_ids", [12])
|
| 37 |
+
self.image_special_token = kwargs.get("image_special_token", "<|image_pad|>")
|
| 38 |
+
self.image_hidden_size = kwargs.get("image_hidden_size", 768)
|
| 39 |
+
self.image_token_len = kwargs.get("image_token_len", 64)
|
| 40 |
+
self.bridge_layer = kwargs.get("bridge_layer", self.num_hidden_layers // 2 - 1)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class TalkerHead(nn.Module):
|
| 44 |
+
def __init__(self, in_features, out_features, num_layers=8, rank=256):
|
| 45 |
+
super().__init__()
|
| 46 |
+
self.num_layers = num_layers
|
| 47 |
+
self.base = nn.Linear(in_features, out_features, bias=False)
|
| 48 |
+
self.adapters = nn.ModuleList([
|
| 49 |
+
nn.Sequential(nn.Linear(in_features, rank, bias=False), nn.GELU(), nn.Linear(rank, out_features, bias=False))
|
| 50 |
+
for _ in range(num_layers)
|
| 51 |
+
])
|
| 52 |
+
|
| 53 |
+
def forward(self, x):
|
| 54 |
+
base_out = self.base(x)
|
| 55 |
+
return [base_out + adapter(x) for adapter in self.adapters]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class TalkerEmbedding(nn.Module):
|
| 59 |
+
def __init__(self, num_embeddings, embedding_dim, num_layers=8, rank=256):
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.num_layers = num_layers
|
| 62 |
+
self.base = nn.Embedding(num_embeddings, embedding_dim)
|
| 63 |
+
self.adapters = nn.ModuleList([
|
| 64 |
+
nn.Sequential(nn.Embedding(num_embeddings, rank), nn.GELU(), nn.Linear(rank, embedding_dim, bias=False))
|
| 65 |
+
for _ in range(num_layers)
|
| 66 |
+
])
|
| 67 |
+
|
| 68 |
+
def forward(self, x):
|
| 69 |
+
base_out = self.base(x)
|
| 70 |
+
return sum(base_out[:, i, :] + self.adapters[i](x[:, i, :]) for i in range(len(self.adapters))) / self.num_layers
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class TalkerModule(nn.Module):
|
| 74 |
+
def __init__(self, config: OmniConfig):
|
| 75 |
+
super().__init__()
|
| 76 |
+
self.talker_config = MiniMindConfig(hidden_size=config.talker_hidden_size, use_moe=config.use_moe)
|
| 77 |
+
self.layers = nn.ModuleList([MiniMindBlock(l, self.talker_config) for l in range(config.num_talker_hidden_layers)])
|
| 78 |
+
self.norm = RMSNorm(config.talker_hidden_size, eps=config.rms_norm_eps)
|
| 79 |
+
self.lm_head = TalkerHead(config.talker_hidden_size, config.audio_vocab_size)
|
| 80 |
+
self.embed_tokens = TalkerEmbedding(config.audio_vocab_size, config.talker_hidden_size)
|
| 81 |
+
self.codec_proj = nn.Sequential(
|
| 82 |
+
nn.Linear(config.talker_hidden_size, config.talker_hidden_size),
|
| 83 |
+
nn.GELU(),
|
| 84 |
+
nn.Linear(config.talker_hidden_size, config.talker_hidden_size),
|
| 85 |
+
RMSNorm(config.talker_hidden_size, eps=config.rms_norm_eps),
|
| 86 |
+
)
|
| 87 |
+
self.embed_proj = nn.Sequential(
|
| 88 |
+
nn.Linear(config.hidden_size, config.hidden_size),
|
| 89 |
+
nn.GELU(),
|
| 90 |
+
nn.Linear(config.hidden_size, config.talker_hidden_size),
|
| 91 |
+
RMSNorm(config.talker_hidden_size, eps=config.rms_norm_eps),
|
| 92 |
+
)
|
| 93 |
+
self.text_scale, self.audio_scale = nn.Parameter(torch.tensor(3.0)), nn.Parameter(torch.tensor(1.0))
|
| 94 |
+
self.spk_proj = nn.Linear(config.spk_emb_size, config.talker_hidden_size, bias=False)
|
| 95 |
+
freqs_cos, freqs_sin = precompute_freqs_cis(
|
| 96 |
+
dim=self.talker_config.head_dim, end=config.max_position_embeddings,
|
| 97 |
+
rope_base=config.rope_theta, rope_scaling=config.rope_scaling
|
| 98 |
+
)
|
| 99 |
+
self.register_buffer("freqs_cos", freqs_cos, persistent=False)
|
| 100 |
+
self.register_buffer("freqs_sin", freqs_sin, persistent=False)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class MiniMindOmni(MiniMindForCausalLM):
|
| 104 |
+
config_class = OmniConfig
|
| 105 |
+
|
| 106 |
+
def __init__(self, config: OmniConfig = None, audio_encoder_path: str = None, vision_model_path: str = None):
|
| 107 |
+
config = config or OmniConfig()
|
| 108 |
+
super().__init__(config)
|
| 109 |
+
object.__setattr__(self, 'thinker', self.model)
|
| 110 |
+
object.__setattr__(self.model, 'lm_head', self.lm_head)
|
| 111 |
+
self.talker = TalkerModule(config)
|
| 112 |
+
self.audio_proj = MMAudioProjector(config.audio_hidden_size, config.hidden_size)
|
| 113 |
+
self.vision_proj = MMVisionProjector(config.image_hidden_size, config.hidden_size, target_tokens=config.image_token_len)
|
| 114 |
+
self.audio_pad_token, self.audio_stop_token, self.audio_spk_token = config.audio_pad_token, config.audio_stop_token, config.audio_spk_token
|
| 115 |
+
audio_encoder = SenseVoiceAudioEncoder(audio_encoder_path) if audio_encoder_path else SenseVoiceAudioEncoder()
|
| 116 |
+
object.__setattr__(self, 'audio_encoder', audio_encoder)
|
| 117 |
+
object.__setattr__(self, 'audio_processor', audio_encoder.processor)
|
| 118 |
+
vision_encoder = SiglipVisionEncoder(vision_model_path) if vision_model_path else SiglipVisionEncoder()
|
| 119 |
+
object.__setattr__(self, 'vision_encoder', vision_encoder)
|
| 120 |
+
object.__setattr__(self, 'vision_processor', vision_encoder.processor)
|
| 121 |
+
|
| 122 |
+
@staticmethod
|
| 123 |
+
def load_sensevoice(path):
|
| 124 |
+
if not os.path.exists(path):
|
| 125 |
+
warnings.warn(f"[MiniMindOmni] SenseVoice path not found: {path}")
|
| 126 |
+
return None, None
|
| 127 |
+
logging.getLogger().setLevel(logging.ERROR)
|
| 128 |
+
hf_logging.set_verbosity_error()
|
| 129 |
+
with contextlib.redirect_stdout(io.StringIO()):
|
| 130 |
+
from funasr import AutoModel
|
| 131 |
+
m = AutoModel(model=path, trust_remote_code=True, disable_update=True, device="cpu")
|
| 132 |
+
encoder, frontend = m.model.encoder, m.kwargs["frontend"]
|
| 133 |
+
for p in encoder.parameters():
|
| 134 |
+
p.requires_grad = False
|
| 135 |
+
return encoder.eval().float(), SenseVoiceAudioProcessor(frontend.eval())
|
| 136 |
+
|
| 137 |
+
@staticmethod
|
| 138 |
+
def load_vision(path):
|
| 139 |
+
if path is None or not os.path.exists(path):
|
| 140 |
+
warnings.warn(f"[MiniMindOmni] Vision model path not found: {path}. vision_encoder will be None!")
|
| 141 |
+
return None, None
|
| 142 |
+
hf_logging.set_verbosity_error()
|
| 143 |
+
try:
|
| 144 |
+
model = SiglipVisionModel.from_pretrained(path)
|
| 145 |
+
except (RuntimeError, ValueError):
|
| 146 |
+
return None, None
|
| 147 |
+
processor = SiglipImageProcessor.from_pretrained(path)
|
| 148 |
+
for p in model.parameters():
|
| 149 |
+
p.requires_grad = False
|
| 150 |
+
return model.eval(), processor
|
| 151 |
+
|
| 152 |
+
@torch.compiler.disable
|
| 153 |
+
def encode_audio_inputs(self, audio_inputs, audio_lens=None):
|
| 154 |
+
if (audio_inputs is None) or (self.audio_encoder is None) or (not audio_inputs.any()):
|
| 155 |
+
return None
|
| 156 |
+
batch_mask = audio_inputs.flatten(1).any(1)
|
| 157 |
+
enc_dtype = next(self.audio_encoder.parameters()).dtype
|
| 158 |
+
valid_fbank = audio_inputs[batch_mask].to(dtype=enc_dtype)
|
| 159 |
+
if audio_lens is not None:
|
| 160 |
+
valid_lens = audio_lens[batch_mask].to(valid_fbank.device)
|
| 161 |
+
else:
|
| 162 |
+
valid_lens = torch.tensor([valid_fbank.size(1)] * valid_fbank.size(0), device=valid_fbank.device)
|
| 163 |
+
with torch.no_grad():
|
| 164 |
+
emb, _ = self.audio_encoder.model(valid_fbank, valid_lens)
|
| 165 |
+
proj_dtype = next(self.audio_proj.parameters()).dtype
|
| 166 |
+
emb_list = [self.audio_proj(emb[i, :max(1, min(valid_lens[i].item(), emb.size(1)))].unsqueeze(0).to(proj_dtype)).squeeze(0) for i in range(emb.size(0))]
|
| 167 |
+
if batch_mask.all():
|
| 168 |
+
return emb_list
|
| 169 |
+
out = [None] * audio_inputs.size(0)
|
| 170 |
+
j = 0
|
| 171 |
+
for i in range(audio_inputs.size(0)):
|
| 172 |
+
if batch_mask[i]:
|
| 173 |
+
out[i] = emb_list[j]
|
| 174 |
+
j += 1
|
| 175 |
+
return out
|
| 176 |
+
|
| 177 |
+
@torch.compiler.disable
|
| 178 |
+
def inject_audio_features(self, tokens, h, audio_feats, seqlen):
|
| 179 |
+
if audio_feats is None or not self.config.audio_ids:
|
| 180 |
+
return h
|
| 181 |
+
marker = self.config.audio_ids[0]
|
| 182 |
+
out = []
|
| 183 |
+
for b in range(h.size(0)):
|
| 184 |
+
hb, seq, i = h[b], tokens[b].tolist(), 0
|
| 185 |
+
af = audio_feats[b] if audio_feats[b] is not None else None
|
| 186 |
+
while i < len(seq):
|
| 187 |
+
if seq[i] == marker:
|
| 188 |
+
start = i
|
| 189 |
+
while i < len(seq) and seq[i] == marker:
|
| 190 |
+
i += 1
|
| 191 |
+
if af is not None:
|
| 192 |
+
inject_len = min(af.size(0), i - start)
|
| 193 |
+
hb = torch.cat((hb[:start], af[:inject_len], hb[start + inject_len:]), dim=0)
|
| 194 |
+
af = None
|
| 195 |
+
else:
|
| 196 |
+
i += 1
|
| 197 |
+
out.append(hb)
|
| 198 |
+
return torch.stack(out)
|
| 199 |
+
|
| 200 |
+
@torch.compiler.disable
|
| 201 |
+
def get_image_embeddings(self, image_inputs):
|
| 202 |
+
if hasattr(image_inputs, 'keys'):
|
| 203 |
+
image_inputs = {k: (v.squeeze(1) if v.ndim > 2 and v.shape[1] == 1 else v) for k, v in image_inputs.items()}
|
| 204 |
+
pixel_attention_mask = image_inputs.get('pixel_attention_mask')
|
| 205 |
+
if pixel_attention_mask is not None and not pixel_attention_mask.any():
|
| 206 |
+
pv = image_inputs['pixel_values']
|
| 207 |
+
return pv.new_zeros(pv.size(0), pv.size(1), self.config.image_hidden_size)
|
| 208 |
+
with torch.no_grad():
|
| 209 |
+
outputs = self.vision_encoder.model(**image_inputs)
|
| 210 |
+
return outputs.last_hidden_state
|
| 211 |
+
|
| 212 |
+
@torch.compiler.disable
|
| 213 |
+
def encode_image_inputs(self, pixel_values):
|
| 214 |
+
if pixel_values is None or self.vision_encoder is None:
|
| 215 |
+
return None
|
| 216 |
+
mask = pixel_values.flatten(1).any(1)
|
| 217 |
+
if not mask.any():
|
| 218 |
+
return pixel_values.new_zeros(pixel_values.size(0), self.config.image_token_len, self.config.hidden_size)
|
| 219 |
+
with torch.no_grad():
|
| 220 |
+
emb = self.vision_encoder.model(pixel_values=pixel_values[mask]).last_hidden_state
|
| 221 |
+
if emb.dim() == 2:
|
| 222 |
+
emb = emb.unsqueeze(0)
|
| 223 |
+
emb = self.vision_proj(emb)
|
| 224 |
+
if mask.all():
|
| 225 |
+
return emb
|
| 226 |
+
idx = mask.nonzero().view(-1, 1, 1).expand_as(emb)
|
| 227 |
+
return emb.new_zeros(pixel_values.size(0), *emb.shape[1:]).scatter(0, idx, emb)
|
| 228 |
+
|
| 229 |
+
@torch.compiler.disable
|
| 230 |
+
def count_vision_proj(self, tokens, h, vision_tensors=None, seqlen=512):
|
| 231 |
+
if vision_tensors is None or not self.config.image_ids:
|
| 232 |
+
return h
|
| 233 |
+
marker, vf = self.config.image_ids[0], vision_tensors
|
| 234 |
+
if vf.dim() == 3:
|
| 235 |
+
vf = vf.unsqueeze(1)
|
| 236 |
+
out = []
|
| 237 |
+
for b in range(h.size(0)):
|
| 238 |
+
hb, seq, k, i = h[b], tokens[b].tolist(), 0, 0
|
| 239 |
+
while i < len(seq):
|
| 240 |
+
if seq[i] == marker:
|
| 241 |
+
start = i
|
| 242 |
+
while i < len(seq) and seq[i] == marker:
|
| 243 |
+
i += 1
|
| 244 |
+
if k < vf.size(1):
|
| 245 |
+
hb = torch.cat((hb[:start], vf[b][k][:i - start], hb[i:]), dim=0)[:seqlen]
|
| 246 |
+
k += 1
|
| 247 |
+
else:
|
| 248 |
+
i += 1
|
| 249 |
+
out.append(hb)
|
| 250 |
+
return torch.stack(out)
|
| 251 |
+
|
| 252 |
+
def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, logits_to_keep=0,
|
| 253 |
+
audio_inputs=None, audio_lens=None, pixel_values=None, **args):
|
| 254 |
+
if len(input_ids.shape) == 2:
|
| 255 |
+
batch_size, seq_length = input_ids.shape
|
| 256 |
+
text_ids = input_ids
|
| 257 |
+
audio_ids = torch.full((batch_size, 8, seq_length), self.audio_pad_token, dtype=torch.long, device=input_ids.device)
|
| 258 |
+
else:
|
| 259 |
+
batch_size, _, seq_length = input_ids.shape
|
| 260 |
+
text_ids, audio_ids = input_ids[:, 8, :], input_ids[:, :8, :]
|
| 261 |
+
if hasattr(past_key_values, 'layers'):
|
| 262 |
+
past_key_values = None
|
| 263 |
+
n_thinker, n_talker = len(self.thinker.layers), len(self.talker.layers)
|
| 264 |
+
past_key_values = past_key_values or ([None] * (n_thinker + n_talker))
|
| 265 |
+
start_pos = past_key_values[0][0].shape[1] if past_key_values[0] is not None else 0
|
| 266 |
+
if self.thinker.freqs_cos[0, 0] == 0:
|
| 267 |
+
freqs_cos, freqs_sin = precompute_freqs_cis(dim=self.config.head_dim, end=self.config.max_position_embeddings, rope_base=self.config.rope_theta, rope_scaling=self.config.rope_scaling)
|
| 268 |
+
self.thinker.freqs_cos, self.thinker.freqs_sin = freqs_cos.to(input_ids.device), freqs_sin.to(input_ids.device)
|
| 269 |
+
if self.talker.freqs_cos[0, 0] == 0:
|
| 270 |
+
freqs_cos, freqs_sin = precompute_freqs_cis(dim=self.talker.talker_config.head_dim, end=self.config.max_position_embeddings, rope_base=self.config.rope_theta, rope_scaling=self.config.rope_scaling)
|
| 271 |
+
self.talker.freqs_cos, self.talker.freqs_sin = freqs_cos.to(input_ids.device), freqs_sin.to(input_ids.device)
|
| 272 |
+
presents = []
|
| 273 |
+
|
| 274 |
+
hidden_states = self.thinker.dropout(self.thinker.embed_tokens(text_ids))
|
| 275 |
+
position_embeddings = (self.thinker.freqs_cos[start_pos:start_pos + seq_length], self.thinker.freqs_sin[start_pos:start_pos + seq_length])
|
| 276 |
+
if audio_inputs is not None and start_pos == 0:
|
| 277 |
+
audio_features = self.encode_audio_inputs(audio_inputs, audio_lens)
|
| 278 |
+
hidden_states = self.inject_audio_features(text_ids, hidden_states, audio_features, seq_length)
|
| 279 |
+
if pixel_values is not None and start_pos == 0:
|
| 280 |
+
if hasattr(pixel_values, 'keys'):
|
| 281 |
+
img_emb = self.get_image_embeddings(pixel_values).to(hidden_states.dtype)
|
| 282 |
+
vision_tensors = self.vision_proj(img_emb)
|
| 283 |
+
else:
|
| 284 |
+
if len(pixel_values.shape) == 6:
|
| 285 |
+
pixel_values = pixel_values.squeeze(2)
|
| 286 |
+
if len(pixel_values.shape) == 4:
|
| 287 |
+
pixel_values = pixel_values.unsqueeze(1)
|
| 288 |
+
bs, num, c, im_h, im_w = pixel_values.shape
|
| 289 |
+
stack_dim = 1 if bs > 1 else 0
|
| 290 |
+
vision_tensors = torch.stack([self.encode_image_inputs(pixel_values[:, i, :, :, :]) for i in range(num)], dim=stack_dim)
|
| 291 |
+
hidden_states = self.count_vision_proj(tokens=text_ids, h=hidden_states, vision_tensors=vision_tensors, seqlen=seq_length)
|
| 292 |
+
bridge_states = hidden_states
|
| 293 |
+
for i, (layer, past_key_value) in enumerate(zip(self.thinker.layers, past_key_values[:n_thinker])):
|
| 294 |
+
hidden_states, present = layer(hidden_states, position_embeddings, past_key_value=past_key_value, use_cache=use_cache, attention_mask=attention_mask)
|
| 295 |
+
presents.append(present)
|
| 296 |
+
if i == self.config.bridge_layer:
|
| 297 |
+
bridge_states = hidden_states
|
| 298 |
+
h_thinker = self.thinker.norm(hidden_states)
|
| 299 |
+
|
| 300 |
+
talker_emb = self.talker.embed_tokens(audio_ids)
|
| 301 |
+
spk_emb = args.get('spk_emb', None)
|
| 302 |
+
if spk_emb is not None:
|
| 303 |
+
spk_mask = (audio_ids[:, 0, :] == self.audio_spk_token).unsqueeze(-1)
|
| 304 |
+
talker_emb = torch.where(spk_mask, self.talker.spk_proj(spk_emb).unsqueeze(1), talker_emb)
|
| 305 |
+
hidden_states = self.talker.embed_proj(bridge_states) * self.talker.text_scale + self.talker.codec_proj(talker_emb) * self.talker.audio_scale
|
| 306 |
+
talker_pos_emb = (self.talker.freqs_cos[start_pos:start_pos + seq_length], self.talker.freqs_sin[start_pos:start_pos + seq_length])
|
| 307 |
+
for layer, past_key_value in zip(self.talker.layers, past_key_values[n_thinker:]):
|
| 308 |
+
hidden_states, present = layer(hidden_states, talker_pos_emb, past_key_value=past_key_value, use_cache=use_cache, attention_mask=attention_mask)
|
| 309 |
+
presents.append(present)
|
| 310 |
+
h_talker = self.talker.norm(hidden_states)
|
| 311 |
+
|
| 312 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 313 |
+
aux_loss = sum(l.mlp.aux_loss for l in list(self.thinker.layers) + list(self.talker.layers) if isinstance(l.mlp, MOEFeedForward))
|
| 314 |
+
aux_loss += sum(p.sum() for p in self.audio_proj.parameters()) * 0 + sum(p.sum() for p in self.vision_proj.parameters()) * 0 + sum(p.sum() for p in self.talker.lm_head.adapters.parameters()) * 0 + sum(p.sum() for p in self.talker.spk_proj.parameters()) * 0
|
| 315 |
+
text_logits = self.thinker.lm_head(h_thinker[:, slice_indices, :])
|
| 316 |
+
audio_logits = self.talker.lm_head(h_talker[:, slice_indices, :])
|
| 317 |
+
|
| 318 |
+
out = MoeCausalLMOutputWithPast(aux_loss=aux_loss, logits=text_logits, past_key_values=presents)
|
| 319 |
+
out.audio_logits = audio_logits
|
| 320 |
+
return out
|
| 321 |
+
|
| 322 |
+
@torch.inference_mode()
|
| 323 |
+
def generate(self, input_ids, eos_token_id=2, max_new_tokens=1024, temperature=0.75, top_p=0.90,
|
| 324 |
+
stream=False, rp=1., use_cache=True, return_audio_codes=False, **args):
|
| 325 |
+
if stream:
|
| 326 |
+
return self.stream_generate(input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, return_audio_codes, **args)
|
| 327 |
+
tokens = list(self.stream_generate(input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, return_audio_codes, **args))
|
| 328 |
+
return tokens[-1] if tokens else input_ids
|
| 329 |
+
|
| 330 |
+
def stream_generate(self, input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, return_audio_codes=False, **args):
|
| 331 |
+
start_pos, past_kvs, text_finished, first_finished = input_ids.shape[1], None, False, True
|
| 332 |
+
audio_codes = [[] for _ in range(8)]
|
| 333 |
+
audio_stop_pos = [None] * 8
|
| 334 |
+
audio_buffer = torch.full((1, 8, start_pos), self.audio_pad_token, dtype=torch.long, device=input_ids.device)
|
| 335 |
+
spk_emb = args.get('spk_emb', None)
|
| 336 |
+
ref_codes = args.get('ref_codes', None)
|
| 337 |
+
ref_len = ref_codes.shape[2] if ref_codes is not None else 0
|
| 338 |
+
spk_reserve = 1 if spk_emb is not None else 0
|
| 339 |
+
fill_end = start_pos
|
| 340 |
+
fill_start = max(spk_reserve, start_pos - ref_len)
|
| 341 |
+
if ref_codes is not None and fill_start < fill_end:
|
| 342 |
+
audio_buffer[:, :, fill_start:fill_end] = ref_codes[:, :, -(fill_end - fill_start):]
|
| 343 |
+
if spk_emb is not None and fill_start > 0:
|
| 344 |
+
audio_buffer[:, :, fill_start - 1] = self.audio_spk_token
|
| 345 |
+
think_end_step, generated_tokens = None, ([] if args.get('open_thinking', False) else None)
|
| 346 |
+
while input_ids.shape[1] < start_pos + max_new_tokens:
|
| 347 |
+
if past_kvs is None or not use_cache:
|
| 348 |
+
out = self.forward(torch.cat((audio_buffer, input_ids.unsqueeze(1)), dim=1), past_key_values=past_kvs, use_cache=use_cache, **args)
|
| 349 |
+
else:
|
| 350 |
+
out = self.forward(torch.cat((audio_buffer[:, :, -1:], input_ids[:, -1:].unsqueeze(1)), dim=1), past_key_values=past_kvs, use_cache=use_cache, **args)
|
| 351 |
+
past_kvs = out.past_key_values
|
| 352 |
+
|
| 353 |
+
logits = out.logits[0, -1, :].clone() / (temperature + 1e-9)
|
| 354 |
+
if rp != 1.0:
|
| 355 |
+
seen = list(set(input_ids[0].tolist()))
|
| 356 |
+
score = logits[seen]
|
| 357 |
+
logits[seen] = torch.where(score > 0, score / rp, score * rp)
|
| 358 |
+
if top_p and top_p < 1.0:
|
| 359 |
+
sorted_l, sorted_i = torch.sort(logits, descending=True)
|
| 360 |
+
mask = torch.cumsum(F.softmax(sorted_l, dim=-1), dim=-1) > top_p
|
| 361 |
+
mask[1:], mask[0] = mask[:-1].clone(), False
|
| 362 |
+
logits[sorted_i[mask]] = -float('Inf')
|
| 363 |
+
text_token = torch.multinomial(F.softmax(logits, dim=-1), 1).item()
|
| 364 |
+
|
| 365 |
+
if text_finished:
|
| 366 |
+
text_token = args.get('enter_token_id', 201) if first_finished else args.get('pad_token_id', 0)
|
| 367 |
+
first_finished = False
|
| 368 |
+
|
| 369 |
+
step = input_ids.shape[1] - start_pos
|
| 370 |
+
audio_step = step - 1
|
| 371 |
+
if generated_tokens is not None:
|
| 372 |
+
generated_tokens.append(text_token)
|
| 373 |
+
if not think_end_step and generated_tokens[-len(self.config.think_end_ids):] == list(self.config.think_end_ids):
|
| 374 |
+
think_end_step = step + 2
|
| 375 |
+
audio_step = (step - think_end_step) if think_end_step else -1
|
| 376 |
+
for i, al in enumerate(out.audio_logits):
|
| 377 |
+
if audio_step < i:
|
| 378 |
+
audio_codes[i].append(self.audio_pad_token)
|
| 379 |
+
else:
|
| 380 |
+
logits_i = al[0, -1, :].clone() / 0.2
|
| 381 |
+
for prev_code in audio_codes[i][-3:]:
|
| 382 |
+
score = logits_i[prev_code]
|
| 383 |
+
logits_i[prev_code] = torch.where(score > 0, score / 1.05, score * 1.05)
|
| 384 |
+
top_val, top_idx = logits_i.topk(50)
|
| 385 |
+
code = top_idx[torch.multinomial(F.softmax(top_val, dim=-1), 1)].item()
|
| 386 |
+
audio_codes[i].append(code)
|
| 387 |
+
if audio_stop_pos[i] is None and code >= 2048:
|
| 388 |
+
audio_stop_pos[i] = len(audio_codes[i]) - 1
|
| 389 |
+
|
| 390 |
+
if text_finished and all(audio_stop_pos[i] is not None for i in range(8)):
|
| 391 |
+
break
|
| 392 |
+
|
| 393 |
+
input_ids = torch.cat((input_ids, torch.tensor([[text_token]], device=input_ids.device)), dim=1)
|
| 394 |
+
audio_buffer = torch.cat((audio_buffer, torch.full((1, 8, 1), self.audio_pad_token, dtype=torch.long, device=input_ids.device)), dim=2)
|
| 395 |
+
for i in range(min(audio_step + 1, 8)):
|
| 396 |
+
audio_buffer[0, i, -1] = audio_codes[i][-1]
|
| 397 |
+
|
| 398 |
+
audio_frame = None
|
| 399 |
+
if return_audio_codes and audio_step >= 7:
|
| 400 |
+
frame = [audio_codes[i][step - 7 + i] for i in range(8)]
|
| 401 |
+
active_layers = sum(1 for i in range(8) if audio_stop_pos[i] is None or step - 7 + i < audio_stop_pos[i])
|
| 402 |
+
if active_layers >= 8:
|
| 403 |
+
audio_frame = frame
|
| 404 |
+
if not text_finished:
|
| 405 |
+
yield input_ids[:, start_pos:], audio_frame
|
| 406 |
+
if text_token == eos_token_id:
|
| 407 |
+
text_finished = True
|
| 408 |
+
else:
|
| 409 |
+
yield None, audio_frame
|
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import warnings
|
| 5 |
+
from typing import Optional, Tuple, List, Union
|
| 6 |
+
from torch import nn
|
| 7 |
+
from transformers.modeling_outputs import MoeCausalLMOutputWithPast
|
| 8 |
+
|
| 9 |
+
from omni.core import precompute_freqs_cis, MOEFeedForward
|
| 10 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 11 |
+
from omni.encoders.vision import SiglipVisionEncoder
|
| 12 |
+
from omni.projectors import MMVisionProjector
|
| 13 |
+
|
| 14 |
+
warnings.filterwarnings('ignore')
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class VLMConfig(MiniMindConfig):
|
| 18 |
+
model_type = "minimind-v"
|
| 19 |
+
|
| 20 |
+
def __init__(self, image_special_token='<|image_pad|>', image_ids=[12], **kwargs):
|
| 21 |
+
self.image_special_token = image_special_token
|
| 22 |
+
self.image_ids = image_ids
|
| 23 |
+
self.image_hidden_size = kwargs.get("image_hidden_size", 768)
|
| 24 |
+
self.image_token_len = kwargs.get("image_token_len", 64)
|
| 25 |
+
super().__init__(**kwargs)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class MiniMindVLM(MiniMindForCausalLM):
|
| 29 |
+
config_class = VLMConfig
|
| 30 |
+
|
| 31 |
+
def __init__(self, config: VLMConfig = None, vision_model_path: Optional[str] = None):
|
| 32 |
+
self.config = config or VLMConfig()
|
| 33 |
+
super().__init__(self.config)
|
| 34 |
+
self.vision_encoder = SiglipVisionEncoder(vision_model_path) if vision_model_path else SiglipVisionEncoder()
|
| 35 |
+
self.vision_proj = MMVisionProjector(
|
| 36 |
+
self.config.image_hidden_size, self.config.hidden_size, target_tokens=self.config.image_token_len
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
@staticmethod
|
| 40 |
+
def get_image_embeddings(image_inputs, vision_model):
|
| 41 |
+
if vision_model is None:
|
| 42 |
+
return None
|
| 43 |
+
return vision_model.encode(image_inputs)
|
| 44 |
+
|
| 45 |
+
@torch.compiler.disable
|
| 46 |
+
def count_vision_proj(self, tokens, h, vision_tensors=None, seqlen=512):
|
| 47 |
+
if vision_tensors is None or not self.config.image_ids:
|
| 48 |
+
return h
|
| 49 |
+
marker, vf = self.config.image_ids[0], vision_tensors
|
| 50 |
+
if vf.dim() == 3:
|
| 51 |
+
vf = vf.unsqueeze(1)
|
| 52 |
+
out = []
|
| 53 |
+
for b in range(h.size(0)):
|
| 54 |
+
hb, seq, k, i = h[b], tokens[b].tolist(), 0, 0
|
| 55 |
+
while i < len(seq):
|
| 56 |
+
if seq[i] == marker:
|
| 57 |
+
start = i
|
| 58 |
+
while i < len(seq) and seq[i] == marker:
|
| 59 |
+
i += 1
|
| 60 |
+
if k < vf.size(1):
|
| 61 |
+
hb = torch.cat((hb[:start], vf[b][k][:i - start], hb[i:]), dim=0)[:seqlen]
|
| 62 |
+
k += 1
|
| 63 |
+
else:
|
| 64 |
+
i += 1
|
| 65 |
+
out.append(hb)
|
| 66 |
+
return torch.stack(out)
|
| 67 |
+
|
| 68 |
+
def forward(self,
|
| 69 |
+
input_ids: Optional[torch.Tensor] = None,
|
| 70 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 71 |
+
past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
|
| 72 |
+
use_cache: bool = False,
|
| 73 |
+
logits_to_keep: Union[int, torch.Tensor] = 0,
|
| 74 |
+
labels: Optional[torch.Tensor] = None,
|
| 75 |
+
pixel_values: Optional[torch.FloatTensor] = None,
|
| 76 |
+
**args):
|
| 77 |
+
batch_size, seq_length = input_ids.shape
|
| 78 |
+
if hasattr(past_key_values, 'layers'):
|
| 79 |
+
past_key_values = None
|
| 80 |
+
past_key_values = past_key_values or [None] * len(self.model.layers)
|
| 81 |
+
start_pos = past_key_values[0][0].shape[1] if past_key_values[0] is not None else 0
|
| 82 |
+
|
| 83 |
+
hidden_states = self.model.dropout(self.model.embed_tokens(input_ids))
|
| 84 |
+
|
| 85 |
+
if pixel_values is not None and start_pos == 0:
|
| 86 |
+
if hasattr(pixel_values, 'keys'):
|
| 87 |
+
sample_val = next(iter(pixel_values.values()))
|
| 88 |
+
if sample_val.ndim == 5:
|
| 89 |
+
bs, num = sample_val.shape[:2]
|
| 90 |
+
vision_tensors = self.vision_proj(
|
| 91 |
+
MiniMindVLM.get_image_embeddings(
|
| 92 |
+
{k: v.flatten(0, 1) for k, v in pixel_values.items()}, self.vision_encoder
|
| 93 |
+
)
|
| 94 |
+
).view(bs, num, self.config.image_token_len, -1)
|
| 95 |
+
else:
|
| 96 |
+
vision_tensors = self.vision_proj(
|
| 97 |
+
MiniMindVLM.get_image_embeddings(pixel_values, self.vision_encoder)
|
| 98 |
+
)
|
| 99 |
+
else:
|
| 100 |
+
if len(pixel_values.shape) == 6:
|
| 101 |
+
pixel_values = pixel_values.squeeze(2)
|
| 102 |
+
bs, num, c, im_h, im_w = pixel_values.shape
|
| 103 |
+
vision_tensors = torch.stack(
|
| 104 |
+
[self.vision_proj(MiniMindVLM.get_image_embeddings(pixel_values[:, i, :, :, :], self.vision_encoder))
|
| 105 |
+
for i in range(num)], dim=1
|
| 106 |
+
)
|
| 107 |
+
hidden_states = self.count_vision_proj(
|
| 108 |
+
tokens=input_ids, h=hidden_states, vision_tensors=vision_tensors, seqlen=input_ids.shape[1]
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
if self.model.freqs_cos[0, 0] == 0:
|
| 112 |
+
freqs_cos, freqs_sin = precompute_freqs_cis(
|
| 113 |
+
dim=self.config.head_dim, end=self.config.max_position_embeddings,
|
| 114 |
+
rope_base=self.config.rope_theta, rope_scaling=self.config.rope_scaling
|
| 115 |
+
)
|
| 116 |
+
self.model.freqs_cos, self.model.freqs_sin = freqs_cos.to(hidden_states.device), freqs_sin.to(hidden_states.device)
|
| 117 |
+
position_embeddings = (
|
| 118 |
+
self.model.freqs_cos[start_pos:start_pos + seq_length],
|
| 119 |
+
self.model.freqs_sin[start_pos:start_pos + seq_length]
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
presents = []
|
| 123 |
+
for layer_idx, (layer, past_key_value) in enumerate(zip(self.model.layers, past_key_values)):
|
| 124 |
+
hidden_states, present = layer(
|
| 125 |
+
hidden_states, position_embeddings,
|
| 126 |
+
past_key_value=past_key_value, use_cache=use_cache, attention_mask=attention_mask
|
| 127 |
+
)
|
| 128 |
+
presents.append(present)
|
| 129 |
+
|
| 130 |
+
hidden_states = self.model.norm(hidden_states)
|
| 131 |
+
|
| 132 |
+
aux_loss = sum([l.mlp.aux_loss for l in self.model.layers if isinstance(l.mlp, MOEFeedForward)],
|
| 133 |
+
hidden_states.new_zeros(1).squeeze())
|
| 134 |
+
aux_loss = aux_loss + sum(p.sum() for p in self.vision_proj.parameters()) * 0
|
| 135 |
+
|
| 136 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 137 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 138 |
+
|
| 139 |
+
loss = None
|
| 140 |
+
if labels is not None:
|
| 141 |
+
shift_logits = logits[..., :-1, :].contiguous()
|
| 142 |
+
shift_labels = labels[..., 1:].contiguous()
|
| 143 |
+
loss = F.cross_entropy(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ignore_index=-100)
|
| 144 |
+
|
| 145 |
+
output = MoeCausalLMOutputWithPast(loss=loss, aux_loss=aux_loss, logits=logits, past_key_values=presents, hidden_states=hidden_states)
|
| 146 |
+
return output
|
| 147 |
+
|
| 148 |
+
def generate(self, *args, num_return_sequences=1, **kwargs):
|
| 149 |
+
if num_return_sequences > 1 and 'pixel_values' in kwargs:
|
| 150 |
+
pv = kwargs['pixel_values']
|
| 151 |
+
if hasattr(pv, 'keys'):
|
| 152 |
+
kwargs['pixel_values'] = {k: v.repeat(num_return_sequences, *([1] * (v.ndim - 1))) for k, v in pv.items()}
|
| 153 |
+
else:
|
| 154 |
+
kwargs['pixel_values'] = pv.repeat(num_return_sequences, *([1] * (pv.ndim - 1)))
|
| 155 |
+
return super().generate(*args, num_return_sequences=num_return_sequences, **kwargs)
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.projectors.vision import MMVisionProjector
|
| 2 |
+
from omni.projectors.audio import MMAudioProjector
|
| 3 |
+
|
| 4 |
+
__all__ = ["MMVisionProjector", "MMAudioProjector"]
|
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class MMAudioProjector(nn.Module):
|
| 6 |
+
def __init__(self, in_dim, out_dim):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.mlp = nn.Sequential(
|
| 9 |
+
nn.LayerNorm(in_dim),
|
| 10 |
+
nn.Linear(in_dim, out_dim),
|
| 11 |
+
nn.GELU(),
|
| 12 |
+
nn.Linear(out_dim, out_dim),
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
def forward(self, x):
|
| 16 |
+
return self.mlp(x)
|
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
from torch import nn
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class MMVisionProjector(nn.Module):
|
| 7 |
+
def __init__(self, in_dim, out_dim, source_tokens=64, target_tokens=64):
|
| 8 |
+
super().__init__()
|
| 9 |
+
self.mlp = nn.Sequential(
|
| 10 |
+
nn.LayerNorm(in_dim),
|
| 11 |
+
nn.Linear(in_dim, out_dim),
|
| 12 |
+
nn.GELU(),
|
| 13 |
+
nn.Linear(out_dim, out_dim),
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
def forward(self, x):
|
| 17 |
+
return self.mlp(x)
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import omni.serve
|
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class SileroVAD:
|
| 5 |
+
def __init__(self, path):
|
| 6 |
+
import onnxruntime as ort
|
| 7 |
+
opts = ort.SessionOptions()
|
| 8 |
+
opts.inter_op_num_threads = opts.intra_op_num_threads = 1
|
| 9 |
+
opts.log_severity_level = 4
|
| 10 |
+
self.session = ort.InferenceSession(path, providers=["CPUExecutionProvider"], sess_options=opts)
|
| 11 |
+
self.h, self.c = np.zeros((2, 1, 64), dtype=np.float32), np.zeros((2, 1, 64), dtype=np.float32)
|
| 12 |
+
|
| 13 |
+
def reset(self):
|
| 14 |
+
self.h[:], self.c[:] = 0, 0
|
| 15 |
+
|
| 16 |
+
def __call__(self, chunk, sr=16000):
|
| 17 |
+
out, self.h, self.c = self.session.run(None, {"input": chunk.reshape(1, -1).astype(np.float32), "h": self.h, "c": self.c, "sr": np.array(sr, dtype="int64")})
|
| 18 |
+
return float(out[0][0])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class RealtimeSession:
|
| 22 |
+
def __init__(self, vad_path, sr=16000, threshold=0.8, min_speech_ms=128, min_silence_ms=800):
|
| 23 |
+
self.vad, self.sr, self.threshold = SileroVAD(vad_path), sr, threshold
|
| 24 |
+
self.min_speech, self.min_silence = int(sr * min_speech_ms / 1000), int(sr * min_silence_ms / 1000)
|
| 25 |
+
self.reset()
|
| 26 |
+
|
| 27 |
+
def reset(self):
|
| 28 |
+
self.vad.reset()
|
| 29 |
+
self.buffer, self.ring, self.speaking, self.generating, self.interrupt = [], [], False, False, False
|
| 30 |
+
self.speech_samples = self.silence_samples = self.tail_silence = 0
|
| 31 |
+
|
| 32 |
+
def push_chunk(self, chunk, W=1024):
|
| 33 |
+
for i in range(0, max(len(chunk), 1), W):
|
| 34 |
+
w = chunk[i:i + W]
|
| 35 |
+
if len(w) < W:
|
| 36 |
+
w = np.pad(w, (0, W - len(w)))
|
| 37 |
+
prob = self.vad(w, self.sr)
|
| 38 |
+
if prob > self.threshold:
|
| 39 |
+
self.silence_samples = self.tail_silence = 0
|
| 40 |
+
self.speech_samples += len(w)
|
| 41 |
+
self.buffer.append(w)
|
| 42 |
+
if self.speech_samples >= self.min_speech and not self.speaking:
|
| 43 |
+
self.speaking = True
|
| 44 |
+
self.buffer = self.ring + self.buffer
|
| 45 |
+
self.ring = []
|
| 46 |
+
if self.generating and self.speaking:
|
| 47 |
+
self.interrupt = True
|
| 48 |
+
return 'interrupt'
|
| 49 |
+
elif self.speaking:
|
| 50 |
+
self.silence_samples += len(w)
|
| 51 |
+
self.tail_silence += 1
|
| 52 |
+
self.buffer.append(w)
|
| 53 |
+
if self.silence_samples >= self.min_silence:
|
| 54 |
+
if self.tail_silence > 1:
|
| 55 |
+
del self.buffer[-(self.tail_silence - 1):]
|
| 56 |
+
self.speaking, self.speech_samples, self.silence_samples, self.tail_silence = False, 0, 0, 0
|
| 57 |
+
return 'speech_end'
|
| 58 |
+
else:
|
| 59 |
+
if self.speech_samples > 0:
|
| 60 |
+
self.buffer.clear()
|
| 61 |
+
self.speech_samples = 0
|
| 62 |
+
self.ring = [w]
|
| 63 |
+
return 'listening'
|
| 64 |
+
|
| 65 |
+
def get_audio(self):
|
| 66 |
+
audio = np.concatenate(self.buffer) if self.buffer else np.array([], dtype=np.float32)
|
| 67 |
+
self.buffer.clear()
|
| 68 |
+
return audio
|
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from omni.trainers import pretrain, full_sft, lora, dpo, distillation, ppo, grpo, agent, rollout_engine, train_tokenizer, pretrain_vlm, full_sft_vlm, full_sft_omni
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"pretrain",
|
| 5 |
+
"full_sft",
|
| 6 |
+
"lora",
|
| 7 |
+
"dpo",
|
| 8 |
+
"distillation",
|
| 9 |
+
"ppo",
|
| 10 |
+
"grpo",
|
| 11 |
+
"agent",
|
| 12 |
+
"rollout_engine",
|
| 13 |
+
"train_tokenizer",
|
| 14 |
+
"pretrain_vlm",
|
| 15 |
+
"full_sft_vlm",
|
| 16 |
+
"full_sft_omni",
|
| 17 |
+
]
|
|
@@ -0,0 +1,489 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import re
|
| 6 |
+
import gc
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import random
|
| 10 |
+
import signal
|
| 11 |
+
import argparse
|
| 12 |
+
import warnings
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
import torch.distributed as dist
|
| 16 |
+
from contextlib import nullcontext
|
| 17 |
+
from torch import optim
|
| 18 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 19 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 20 |
+
from torch.optim.lr_scheduler import CosineAnnealingLR
|
| 21 |
+
from transformers import AutoTokenizer
|
| 22 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 23 |
+
from omni.datasets.lm_dataset import AgentRLDataset
|
| 24 |
+
from omni.utils.training import Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, SkipBatchSampler, init_model, LMForRewardModel
|
| 25 |
+
from omni.trainers.rollout_engine import create_rollout_engine, compute_per_token_logps
|
| 26 |
+
|
| 27 |
+
warnings.filterwarnings('ignore')
|
| 28 |
+
|
| 29 |
+
# ================================ 工具与 Reward = Start ================================
|
| 30 |
+
|
| 31 |
+
def rep_penalty(text, n=3, cap=0.5):
|
| 32 |
+
toks = re.findall(r"\w+|[^\w\s]", text.lower())
|
| 33 |
+
grams = [tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)]
|
| 34 |
+
return min(cap, (len(grams) - len(set(grams))) * cap * 2 / len(grams)) if grams else 0.0
|
| 35 |
+
|
| 36 |
+
# ======== 工具定义 ========
|
| 37 |
+
TOOLS = [
|
| 38 |
+
{"type": "function", "function": {"name": "calculate_math", "description": "计算数学表达式", "parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}}},
|
| 39 |
+
{"type": "function", "function": {"name": "unit_converter", "description": "单位换算", "parameters": {"type": "object", "properties": {"value": {"type": "number"}, "from_unit": {"type": "string"}, "to_unit": {"type": "string"}}, "required": ["value", "from_unit", "to_unit"]}}},
|
| 40 |
+
{"type": "function", "function": {"name": "get_current_weather", "description": "获取天气", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}},
|
| 41 |
+
{"type": "function", "function": {"name": "get_current_time", "description": "获取时间", "parameters": {"type": "object", "properties": {"timezone": {"type": "string", "default": "Asia/Shanghai"}}, "required": []}}},
|
| 42 |
+
{"type": "function", "function": {"name": "get_exchange_rate", "description": "查询汇率", "parameters": {"type": "object", "properties": {"from_currency": {"type": "string"}, "to_currency": {"type": "string"}}, "required": ["from_currency", "to_currency"]}}},
|
| 43 |
+
{"type": "function", "function": {"name": "translate_text", "description": "翻译文本", "parameters": {"type": "object", "properties": {"text": {"type": "string"}, "target_language": {"type": "string"}}, "required": ["text", "target_language"]}}},
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# ======== 模拟数据 ========
|
| 47 |
+
WEATHER_DATA = {"北京": ("28°C", "晴"), "上海": ("15°C", "多云"), "广州": ("32°C", "闷热"), "深圳": ("30°C", "晴"), "杭州": ("22°C", "阴"), "成都": ("18°C", "小雨"), "武汉": ("25°C", "多云"), "南京": ("20°C", "晴"), "西安": ("16°C", "大风"), "重庆": ("26°C", "阴"), "Tokyo": ("12°C", "晴"), "New York": ("8°C", "多云"), "London": ("5°C", "小雨"), "Paris": ("10°C", "阴"), "Sydney": ("25°C", "晴朗")}
|
| 48 |
+
TIME_DATA = {"Asia/Shanghai": "2025-03-07 14:30:00", "America/New_York": "2025-03-07 01:30:00", "Europe/London": "2025-03-07 06:30:00", "Asia/Tokyo": "2025-03-07 15:30:00", "Europe/Paris": "2025-03-07 07:30:00", "Australia/Sydney": "2025-03-07 17:30:00"}
|
| 49 |
+
EXCHANGE_DATA = {("USD", "CNY"): 7.21, ("EUR", "CNY"): 7.85, ("GBP", "CNY"): 9.12, ("JPY", "CNY"): 0.048, ("USD", "EUR"): 0.92, ("USD", "GBP"): 0.79, ("CNY", "JPY"): 20.83, ("AUD", "CNY"): 4.72}
|
| 50 |
+
TRANSLATE_DATA = {("你好世界", "english"): "Hello World", ("Good morning", "chinese"): "早上好", ("今天天气真好", "english"): "The weather is nice today", ("I love programming", "chinese"): "我喜欢编程", ("机器学习很有趣", "english"): "Machine learning is interesting", ("Happy birthday", "chinese"): "生日快乐"}
|
| 51 |
+
UNIT_DATA = {"km_miles": 0.621371, "miles_km": 1.60934, "kg_pounds": 2.20462, "pounds_kg": 0.453592, "meters_feet": 3.28084, "feet_meters": 0.3048, "celsius_fahrenheit": 1.8, "fahrenheit_celsius": 0.5556}
|
| 52 |
+
|
| 53 |
+
# ======== 模拟执行 ========
|
| 54 |
+
MOCK_RESULTS = {
|
| 55 |
+
"calculate_math": lambda args: {"result": str(eval(str(args.get("expression", "0")).replace("^", "**").replace("×", "*").replace("÷", "/").replace("−", "-").replace("(", "(").replace(")", ")"), {"__builtins__": {}, "math": math}))},
|
| 56 |
+
"unit_converter": lambda args: {"result": round(float(args.get("value", 0)) * UNIT_DATA.get(f"{args.get('from_unit', '').lower()}_{args.get('to_unit', '').lower()}", 1), 4)},
|
| 57 |
+
"get_current_weather": lambda args: (lambda w: {"city": args.get("location"), "temperature": w[0], "humidity": "65%", "condition": w[1]})(WEATHER_DATA.get(args.get("location"), ("22°C", "晴"))),
|
| 58 |
+
"get_current_time": lambda args: {"datetime": TIME_DATA.get(args.get("timezone", "Asia/Shanghai"), "2025-03-07 14:30:00"), "timezone": args.get("timezone", "Asia/Shanghai")},
|
| 59 |
+
"get_exchange_rate": lambda args: {"from": args.get("from_currency"), "to": args.get("to_currency"), "rate": EXCHANGE_DATA.get((args.get("from_currency"), args.get("to_currency")), 1.0)},
|
| 60 |
+
"translate_text": lambda args: {"translated_text": TRANSLATE_DATA.get((args.get("text"), args.get("target_language")), args.get("text", ""))},
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# ======== 参数校验 ========
|
| 64 |
+
CHECK_ARGS = {
|
| 65 |
+
"calculate_math": lambda a: bool(a.get("expression")),
|
| 66 |
+
"unit_converter": lambda a: a.get("value") is not None and a.get("from_unit") and a.get("to_unit"),
|
| 67 |
+
"get_current_weather": lambda a: bool(a.get("location")),
|
| 68 |
+
"get_current_time": lambda a: True,
|
| 69 |
+
"get_exchange_rate": lambda a: bool(a.get("from_currency")) and bool(a.get("to_currency")),
|
| 70 |
+
"translate_text": lambda a: bool(a.get("text")) and bool(a.get("target_language")),
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# ======== 工具调用解析与执行 ========
|
| 74 |
+
def parse_tool_calls(text):
|
| 75 |
+
calls = []
|
| 76 |
+
for m in re.findall(r'<tool_call>(.*?)</tool_call>', text, re.DOTALL):
|
| 77 |
+
try: calls.append(json.loads(m.strip()))
|
| 78 |
+
except: pass
|
| 79 |
+
return calls
|
| 80 |
+
|
| 81 |
+
def execute_tool(name, args):
|
| 82 |
+
fn = MOCK_RESULTS.get(name)
|
| 83 |
+
if not fn: return None
|
| 84 |
+
try:
|
| 85 |
+
signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(TimeoutError()))
|
| 86 |
+
signal.alarm(1)
|
| 87 |
+
return fn(args)
|
| 88 |
+
except:
|
| 89 |
+
return None
|
| 90 |
+
finally:
|
| 91 |
+
try: signal.alarm(0)
|
| 92 |
+
except: pass
|
| 93 |
+
|
| 94 |
+
# ======== 多轮 Rollout ========
|
| 95 |
+
def rollout_single(rollout_engine, tokenizer, messages, tools, max_turns=3, max_new_tokens=256, thinking_ratio=0.5, device="cuda"):
|
| 96 |
+
all_outputs = []
|
| 97 |
+
prompt_ids = None
|
| 98 |
+
response_ids = []
|
| 99 |
+
response_mask = []
|
| 100 |
+
response_old_logps = []
|
| 101 |
+
final_context = ""
|
| 102 |
+
unfinished = False
|
| 103 |
+
open_thinking = random.random() < thinking_ratio
|
| 104 |
+
for turn in range(max_turns):
|
| 105 |
+
context = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools, open_thinking=open_thinking)
|
| 106 |
+
inputs = tokenizer(context, return_tensors="pt", add_special_tokens=False).to(device)
|
| 107 |
+
context_ids = inputs["input_ids"][0].tolist()
|
| 108 |
+
if prompt_ids is None:
|
| 109 |
+
prompt_ids = context_ids
|
| 110 |
+
rollout_result = rollout_engine.rollout(
|
| 111 |
+
prompt_ids=inputs["input_ids"],
|
| 112 |
+
attention_mask=inputs["attention_mask"],
|
| 113 |
+
num_generations=1,
|
| 114 |
+
max_new_tokens=max_new_tokens,
|
| 115 |
+
temperature=0.8,
|
| 116 |
+
)
|
| 117 |
+
new_ids = rollout_result.completion_ids[0].tolist()
|
| 118 |
+
new_logps = rollout_result.per_token_logps[0].tolist()
|
| 119 |
+
if len(new_ids) != len(new_logps): Logger(f"rollout token/logprob length mismatch: {len(new_ids)} vs {len(new_logps)}")
|
| 120 |
+
pairs = [(t, lp) for t, lp in zip(new_ids, new_logps) if t != tokenizer.pad_token_id and t != tokenizer.eos_token_id]
|
| 121 |
+
new_ids = [t for t, _ in pairs]
|
| 122 |
+
new_logps = [lp for _, lp in pairs]
|
| 123 |
+
new_text = rollout_result.completions[0]
|
| 124 |
+
all_outputs.append(new_text)
|
| 125 |
+
response_ids.extend(new_ids)
|
| 126 |
+
response_mask.extend([1] * len(new_ids))
|
| 127 |
+
response_old_logps.extend(new_logps)
|
| 128 |
+
final_context = context + new_text
|
| 129 |
+
calls = parse_tool_calls(new_text)
|
| 130 |
+
if not calls:
|
| 131 |
+
break
|
| 132 |
+
unfinished = turn == max_turns - 1
|
| 133 |
+
messages.append({"role": "assistant", "content": new_text})
|
| 134 |
+
for call in calls:
|
| 135 |
+
name, raw = call.get("name", ""), call.get("arguments", {})
|
| 136 |
+
if isinstance(raw, str):
|
| 137 |
+
try: raw = json.loads(raw)
|
| 138 |
+
except: raw = {}
|
| 139 |
+
result = execute_tool(name, raw)
|
| 140 |
+
result_str = (json.dumps(result, ensure_ascii=False) if result else '{"error": "tool not found"}')[:2048] # 防止天文数字撑爆tokenizer
|
| 141 |
+
messages.append({"role": "tool", "content": result_str})
|
| 142 |
+
|
| 143 |
+
observe_context = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=not unfinished, tools=tools, open_thinking=open_thinking)
|
| 144 |
+
observe_ids = tokenizer(observe_context, return_tensors="pt", add_special_tokens=False)["input_ids"][0].tolist()
|
| 145 |
+
current_len = len(prompt_ids) + len(response_ids)
|
| 146 |
+
obs_delta = observe_ids[current_len:]
|
| 147 |
+
response_ids.extend(obs_delta)
|
| 148 |
+
response_mask.extend([0] * len(obs_delta))
|
| 149 |
+
response_old_logps.extend([0.0] * len(obs_delta))
|
| 150 |
+
final_context = observe_context
|
| 151 |
+
|
| 152 |
+
final_output = all_outputs[-1] if all_outputs else ""
|
| 153 |
+
prompt_ids = prompt_ids or []
|
| 154 |
+
return final_output, final_context, prompt_ids, response_ids, response_mask, response_old_logps, list(all_outputs), unfinished
|
| 155 |
+
|
| 156 |
+
def rollout_batch(rollout_engine, tokenizer, messages_batch, tools_batch, num_gen, max_turns=3, max_new_tokens=256, thinking_ratio=0.5, device="cuda"):
|
| 157 |
+
all_completions = []
|
| 158 |
+
all_contexts = []
|
| 159 |
+
all_prompt_ids = []
|
| 160 |
+
all_response_ids = []
|
| 161 |
+
all_response_masks = []
|
| 162 |
+
all_response_old_logps = []
|
| 163 |
+
all_turn_outputs = []
|
| 164 |
+
all_unfinished = []
|
| 165 |
+
for messages, tools in zip(messages_batch, tools_batch):
|
| 166 |
+
for _ in range(num_gen):
|
| 167 |
+
msgs_copy = [dict(m) for m in messages]
|
| 168 |
+
completion, context, prompt_ids, response_ids, response_mask, response_old_logps, turn_outputs, unfinished = rollout_single(rollout_engine, tokenizer, msgs_copy, tools, max_turns, max_new_tokens, thinking_ratio, device)
|
| 169 |
+
all_completions.append(completion)
|
| 170 |
+
all_contexts.append(context)
|
| 171 |
+
all_prompt_ids.append(prompt_ids)
|
| 172 |
+
all_response_ids.append(response_ids)
|
| 173 |
+
all_response_masks.append(response_mask)
|
| 174 |
+
all_response_old_logps.append(response_old_logps)
|
| 175 |
+
all_turn_outputs.append(turn_outputs)
|
| 176 |
+
all_unfinished.append(unfinished)
|
| 177 |
+
return all_completions, all_contexts, all_prompt_ids, all_response_ids, all_response_masks, all_response_old_logps, all_turn_outputs, all_unfinished
|
| 178 |
+
|
| 179 |
+
# ======== Reward 计算 ========
|
| 180 |
+
def validate_gt_in_text(text, gt_list):
|
| 181 |
+
text, text_num = str(text), str(text).replace(',', '')
|
| 182 |
+
nums = [float(x) for x in re.findall(r'(?<![\w.])[-+]?\d+(?:\.\d+)?(?![\w.])', text_num)]
|
| 183 |
+
return {g for g in gt_list if ((s := str(g).strip()) and s.lower() in text.lower()) or (re.fullmatch(r'[-+]?\d+(?:\.\d+)?', str(g).strip().replace(',', '')) and any(abs(float(str(g).strip().replace(',', '')) - n) < 1e-6 for n in nums))}
|
| 184 |
+
|
| 185 |
+
def calculate_rewards(prompts, completions, gt_batch, tools_batch, num_gen, reward_model=None, device="cuda", turn_outputs_batch=None, unfinished_batch=None):
|
| 186 |
+
rewards = torch.zeros(len(completions), device=device)
|
| 187 |
+
for idx, response in enumerate(completions):
|
| 188 |
+
reward, answer = 0.0, response
|
| 189 |
+
sample_idx = idx // num_gen
|
| 190 |
+
tools = tools_batch[sample_idx]
|
| 191 |
+
turn_outputs = turn_outputs_batch[idx] if turn_outputs_batch is not None else [response]
|
| 192 |
+
unfinished = unfinished_batch[idx] if unfinished_batch is not None else False
|
| 193 |
+
turn_answers = [turn.split('</think>', 1)[-1].strip() if '</think>' in turn else turn.strip() for turn in turn_outputs]
|
| 194 |
+
answer = turn_answers[-1] if turn_answers else response.strip()
|
| 195 |
+
valid_names = {t['function']['name'] for t in tools} if tools else set()
|
| 196 |
+
tool_calls = []
|
| 197 |
+
for turn_answer in turn_answers: tool_calls.extend(parse_tool_calls(turn_answer)) # 解析tool调用
|
| 198 |
+
reward -= 0.5 * sum(abs(turn.count('<tool_call>') - turn.count('</tool_call>')) for turn in turn_answers) # 标签扣分
|
| 199 |
+
# -------- 无工具调用:格式+reward奖励 --------
|
| 200 |
+
if not tool_calls:
|
| 201 |
+
reward += 0.5 if 5 <= len(response.strip()) <= 800 else -0.5 # 长度分
|
| 202 |
+
if '</think>' in response:
|
| 203 |
+
think, answer = response.split('</think>', 1)
|
| 204 |
+
reward += 1.0 if 20 <= len(think.strip()) <= 300 else -0.5 # 思考长度分
|
| 205 |
+
reward += 0.25 if response.count('</think>') == 1 else -0.25 # 思考闭合分
|
| 206 |
+
answer = answer.strip()
|
| 207 |
+
if reward_model is not None:
|
| 208 |
+
prompt = prompts[sample_idx]
|
| 209 |
+
pattern = r"<\|im_start\|>(system|user|assistant)\s+(.*?)<\|im_end\|>"
|
| 210 |
+
matches = re.findall(pattern, prompt, re.DOTALL)
|
| 211 |
+
messages = [{"role": role, "content": content.strip()} for role, content in matches]
|
| 212 |
+
score = reward_model.get_score(messages, answer)
|
| 213 |
+
reward += score # RM分
|
| 214 |
+
reward -= rep_penalty(answer)
|
| 215 |
+
rewards[idx] = max(min(reward, 3.0), -3.0) # 总分Clip
|
| 216 |
+
# -------- 有工具调用:执行结果奖励 --------
|
| 217 |
+
else:
|
| 218 |
+
gt = gt_batch[sample_idx]
|
| 219 |
+
valid_call_count = 0
|
| 220 |
+
for tool_call in tool_calls:
|
| 221 |
+
name, raw = tool_call.get("name", ""), tool_call.get("arguments", {})
|
| 222 |
+
if isinstance(raw, str):
|
| 223 |
+
try: raw = json.loads(raw)
|
| 224 |
+
except: raw = {}
|
| 225 |
+
check = CHECK_ARGS.get(name)
|
| 226 |
+
valid_call_count += int(bool(name in valid_names and check and check(raw)))
|
| 227 |
+
tool_gap = abs(valid_call_count - len(gt)) + max(0, len(tool_calls) - valid_call_count) # tool数差值
|
| 228 |
+
reward += 0.5 if tool_gap == 0 else -0.5 * tool_gap # tool对齐分
|
| 229 |
+
|
| 230 |
+
final_text = "" if unfinished else (answer.split('</tool_call>')[-1] if '</tool_call>' in answer else answer)
|
| 231 |
+
verified = validate_gt_in_text(final_text, gt) if gt else set()
|
| 232 |
+
if gt: reward += 2.5 * len(verified) / len(gt) # GT分
|
| 233 |
+
if unfinished: reward -= 0.5 # 未完成扣分
|
| 234 |
+
reward -= rep_penalty(final_text if final_text else answer)
|
| 235 |
+
rewards[idx] = max(min(reward, 3.0), -3.0) # 总分Clip
|
| 236 |
+
return rewards
|
| 237 |
+
|
| 238 |
+
# ================================ 工具与 Reward = End ================================
|
| 239 |
+
def rl_train_epoch(epoch, loader, iters, rollout_engine, ref_model, reward_model=None, start_step=0, wandb=None, use_sglang=False):
|
| 240 |
+
last_step = start_step
|
| 241 |
+
for step, batch in enumerate(loader, start=start_step + 1):
|
| 242 |
+
messages_batch = batch['messages']
|
| 243 |
+
tools_batch = batch['tools']
|
| 244 |
+
gt_batch = batch['gt']
|
| 245 |
+
last_step = step
|
| 246 |
+
|
| 247 |
+
with torch.no_grad():
|
| 248 |
+
completions, contexts, prompt_ids_batch, response_ids_batch, response_masks_batch, response_old_logps_batch, turn_outputs_batch, unfinished_batch = rollout_batch(rollout_engine, tokenizer, messages_batch, tools_batch, args.num_generations, max_turns=3, max_new_tokens=args.max_gen_len, thinking_ratio=args.thinking_ratio, device=args.device)
|
| 249 |
+
|
| 250 |
+
prompts = [tokenizer.apply_chat_template(m, tokenize=False, add_generation_prompt=True, tools=t) for m, t in zip(messages_batch, tools_batch)]
|
| 251 |
+
packed_samples = []
|
| 252 |
+
for p, r, m, old_lp in zip(prompt_ids_batch, response_ids_batch, response_masks_batch, response_old_logps_batch):
|
| 253 |
+
ids = p + r
|
| 254 |
+
mask = [0] * len(p) + m
|
| 255 |
+
old_logps = [0.0] * max(len(p) - 1, 0) + old_lp
|
| 256 |
+
if len(ids) > args.max_total_len:
|
| 257 |
+
ids = ids[-args.max_total_len:]
|
| 258 |
+
mask = mask[-args.max_total_len:]
|
| 259 |
+
old_logps = old_logps[-(len(ids) - 1):]
|
| 260 |
+
prompt_len = next((i for i, v in enumerate(mask) if v == 1), len(mask))
|
| 261 |
+
packed_samples.append((ids, mask, prompt_len, old_logps))
|
| 262 |
+
seq_lens = torch.tensor([len(ids) for ids, _, _, _ in packed_samples], device=args.device)
|
| 263 |
+
max_len = seq_lens.max().item()
|
| 264 |
+
input_ids = torch.tensor([ids + [tokenizer.pad_token_id] * (max_len - len(ids)) for ids, _, _, _ in packed_samples], device=args.device)
|
| 265 |
+
prompt_lens = torch.tensor([prompt_len for _, _, prompt_len, _ in packed_samples], device=args.device)
|
| 266 |
+
full_response_masks = torch.tensor([mask + [0] * (max_len - len(mask)) for _, mask, _, _ in packed_samples], device=args.device, dtype=torch.float32)
|
| 267 |
+
old_per_token_logps = torch.tensor([old_logps + [0.0] * ((max_len - 1) - len(old_logps)) for _, _, _, old_logps in packed_samples], device=args.device, dtype=torch.float32)
|
| 268 |
+
full_mask = (input_ids != tokenizer.pad_token_id).long()
|
| 269 |
+
|
| 270 |
+
rewards = calculate_rewards(prompts, completions, gt_batch, tools_batch, args.num_generations, reward_model, device=args.device, turn_outputs_batch=turn_outputs_batch, unfinished_batch=unfinished_batch)
|
| 271 |
+
|
| 272 |
+
model_unwrapped = model.module if isinstance(model, DistributedDataParallel) else model
|
| 273 |
+
with autocast_ctx:
|
| 274 |
+
res = model_unwrapped(input_ids, attention_mask=full_mask)
|
| 275 |
+
aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device)
|
| 276 |
+
logits = res.logits[:, :-1, :]
|
| 277 |
+
per_token_logps = F.log_softmax(logits, dim=-1).gather(2, input_ids[:, 1:].unsqueeze(-1)).squeeze(-1)
|
| 278 |
+
|
| 279 |
+
with torch.no_grad():
|
| 280 |
+
ref_per_token_logps = compute_per_token_logps(ref_model, input_ids, input_ids.size(1) - 1, attention_mask=full_mask)
|
| 281 |
+
|
| 282 |
+
completion_mask = full_response_masks[:, 1:]
|
| 283 |
+
is_eos = (input_ids[:, 1:] == tokenizer.eos_token_id) & completion_mask.bool()
|
| 284 |
+
eos_idx = torch.full((completion_mask.size(0),), completion_mask.size(1) - 1, device=args.device, dtype=torch.long)
|
| 285 |
+
has_eos = is_eos.any(dim=1)
|
| 286 |
+
eos_idx[has_eos] = is_eos.int().argmax(dim=1)[has_eos]
|
| 287 |
+
pos = torch.arange(completion_mask.size(1), device=args.device).unsqueeze(0)
|
| 288 |
+
completion_mask = completion_mask * (pos <= eos_idx.unsqueeze(1)).float()
|
| 289 |
+
token_counts = completion_mask.sum(dim=1)
|
| 290 |
+
valid_rows = token_counts > 0
|
| 291 |
+
|
| 292 |
+
if args.debug_mode and is_main_process() and step % args.debug_interval == 0:
|
| 293 |
+
for i in range(len(messages_batch)):
|
| 294 |
+
Logger(f"[DEBUG] step={step}, gt[{i}]: {repr(gt_batch[i])}")
|
| 295 |
+
Logger('-'*100)
|
| 296 |
+
for j in range(args.num_generations):
|
| 297 |
+
idx = i * args.num_generations + j
|
| 298 |
+
plen, slen = prompt_lens[idx].item(), seq_lens[idx].item()
|
| 299 |
+
Logger(f"{'=' * 30} [DEBUG] gen[{i}][{j}] CONTEXT_BEGIN {'=' * 30}")
|
| 300 |
+
Logger(contexts[idx])
|
| 301 |
+
Logger(f"{'=' * 31} [DEBUG] gen[{i}][{j}] CONTEXT_END {'=' * 31}")
|
| 302 |
+
Logger(f"[DEBUG] gen[{i}][{j}] prompt_len={plen}, seq_len={slen}")
|
| 303 |
+
tokens = input_ids[idx, plen:slen].tolist()
|
| 304 |
+
text = tokenizer.decode(tokens, skip_special_tokens=False)
|
| 305 |
+
Logger(f"{'=' * 28} [DEBUG] gen[{i}][{j}] COMPLETION_BEGIN [{plen}:{slen}] {'=' * 28}")
|
| 306 |
+
Logger(text)
|
| 307 |
+
Logger(f"{'=' * 29} [DEBUG] gen[{i}][{j}] COMPLETION_END {'=' * 29}")
|
| 308 |
+
Logger(f"[DEBUG] gen[{i}][{j}] reward={rewards[idx].item():.4f}")
|
| 309 |
+
Logger('='*100)
|
| 310 |
+
|
| 311 |
+
grouped_rewards = rewards.view(-1, args.num_generations)
|
| 312 |
+
mean_r = grouped_rewards.mean(dim=1).repeat_interleave(args.num_generations)
|
| 313 |
+
std_r = grouped_rewards.std(dim=1, unbiased=False).repeat_interleave(args.num_generations)
|
| 314 |
+
advantages = (rewards - mean_r) / (std_r + 1e-4)
|
| 315 |
+
|
| 316 |
+
kl_div = ref_per_token_logps - per_token_logps
|
| 317 |
+
per_token_kl = torch.exp(kl_div) - kl_div - 1
|
| 318 |
+
ratio = torch.exp(per_token_logps - old_per_token_logps)
|
| 319 |
+
if args.loss_type == "cispo":
|
| 320 |
+
clamped_ratio = torch.clamp(ratio, max=args.epsilon_high).detach()
|
| 321 |
+
per_token_loss = -(clamped_ratio * advantages.unsqueeze(1) * per_token_logps - args.beta * per_token_kl)
|
| 322 |
+
else:
|
| 323 |
+
clipped_ratio = torch.clamp(ratio, 1 - args.epsilon, 1 + args.epsilon)
|
| 324 |
+
per_token_loss1 = ratio * advantages.unsqueeze(1)
|
| 325 |
+
per_token_loss2 = clipped_ratio * advantages.unsqueeze(1)
|
| 326 |
+
per_token_loss = -(torch.min(per_token_loss1, per_token_loss2) - args.beta * per_token_kl)
|
| 327 |
+
policy_loss = (((per_token_loss * completion_mask).sum(dim=1)[valid_rows] / token_counts[valid_rows].clamp(min=1)).mean()
|
| 328 |
+
if valid_rows.any() else per_token_loss.sum() * 0.0)
|
| 329 |
+
loss = (policy_loss + aux_loss) / args.accumulation_steps
|
| 330 |
+
loss.backward()
|
| 331 |
+
|
| 332 |
+
if step % args.accumulation_steps == 0:
|
| 333 |
+
if args.grad_clip > 0: torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 334 |
+
optimizer.step(); scheduler.step(); optimizer.zero_grad()
|
| 335 |
+
|
| 336 |
+
if step % args.log_interval == 0 or step == iters:
|
| 337 |
+
pl = loss.item() * args.accumulation_steps
|
| 338 |
+
ar = rewards.mean().item()
|
| 339 |
+
al = token_counts.float().mean().item()
|
| 340 |
+
kl = ((ref_per_token_logps - per_token_logps) * completion_mask).sum().item() / max(token_counts.sum().item(), 1)
|
| 341 |
+
gs = grouped_rewards.std(dim=1, unbiased=False).mean().item()
|
| 342 |
+
am, ast = advantages.mean().item(), advantages.std().item()
|
| 343 |
+
lr = optimizer.param_groups[0]['lr']
|
| 344 |
+
Logger(f'Epoch:[{epoch+1}/{args.epochs}]({step}/{iters}), Reward:{ar:.4f}, KL:{kl:.4f}, GrpStd:{gs:.4f}, AdvStd:{ast:.4f}, Loss:{pl:.4f}, AvgLen:{al:.2f}, AdvMean:{am:.4f}, LR:{lr:.8f}')
|
| 345 |
+
if wandb and is_main_process():
|
| 346 |
+
wandb.log({"reward":ar,"kl_ref":kl,"group_reward_std":gs,"advantages_std":ast,"policy_loss":pl,"avg_response_len":al,"advantages_mean":am,"learning_rate":lr})
|
| 347 |
+
|
| 348 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 349 |
+
model.eval()
|
| 350 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 351 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 352 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 353 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 354 |
+
state_dict = raw_model.state_dict()
|
| 355 |
+
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
|
| 356 |
+
lm_checkpoint(lm_config, weight=args.save_weight, model=model, optimizer=optimizer,
|
| 357 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints', scheduler=scheduler)
|
| 358 |
+
model.train()
|
| 359 |
+
del state_dict
|
| 360 |
+
|
| 361 |
+
if step % args.save_interval == 0 or step == iters: rollout_engine.update_policy(model)
|
| 362 |
+
|
| 363 |
+
del per_token_logps, ref_per_token_logps
|
| 364 |
+
del completions, rewards, grouped_rewards, mean_r, std_r, advantages, completion_mask
|
| 365 |
+
|
| 366 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 367 |
+
if args.grad_clip > 0: torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 368 |
+
optimizer.step(); scheduler.step(); optimizer.zero_grad()
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
if __name__ == "__main__":
|
| 372 |
+
parser = argparse.ArgumentParser(description="MiniMind Agent RL")
|
| 373 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 374 |
+
parser.add_argument('--save_weight', default='agent', type=str, help="保存权重名称")
|
| 375 |
+
parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
|
| 376 |
+
parser.add_argument("--batch_size", type=int, default=2, help="批次大小")
|
| 377 |
+
parser.add_argument("--learning_rate", type=float, default=3e-7, help="学习率")
|
| 378 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 379 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="数据类型 bfloat16/float16")
|
| 380 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 381 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 382 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 383 |
+
parser.add_argument("--log_interval", type=int, default=1, help="日志打印间隔")
|
| 384 |
+
parser.add_argument("--save_interval", type=int, default=10, help="模型保存间隔")
|
| 385 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="模型隐藏层维度")
|
| 386 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="模型层数")
|
| 387 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE")
|
| 388 |
+
parser.add_argument('--max_seq_len', default=1024, type=int, help="最大序列长度")
|
| 389 |
+
parser.add_argument("--max_gen_len", type=int, default=768, help="单次最大生成长度")
|
| 390 |
+
parser.add_argument("--max_total_len", type=int, default=2500, help="训练侧最终总长度上界")
|
| 391 |
+
parser.add_argument("--data_path", type=str, default="../dataset/agent_rl.jsonl", help="训练数据路径")
|
| 392 |
+
parser.add_argument("--num_generations", type=int, default=4, help="每个prompt生成数量")
|
| 393 |
+
parser.add_argument("--beta", type=float, default=0.1, help="KL散度惩罚系数")
|
| 394 |
+
parser.add_argument("--loss_type", type=str, default="cispo", choices=["grpo", "cispo"], help="loss类型")
|
| 395 |
+
parser.add_argument("--epsilon", type=float, default=0.2, help="GRPO的PPO clip epsilon")
|
| 396 |
+
parser.add_argument("--epsilon_high", type=float, default=5.0, help="epsilon上界")
|
| 397 |
+
parser.add_argument('--from_weight', default='full_sft', type=str, help="加载预训练权重名称")
|
| 398 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否从checkpoint恢复")
|
| 399 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb记录")
|
| 400 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-Agent-RL", help="wandb项目名称")
|
| 401 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile")
|
| 402 |
+
parser.add_argument("--debug_mode", action="store_true", help="调试模式")
|
| 403 |
+
parser.add_argument("--debug_interval", type=int, default=20, help="调试日志间隔")
|
| 404 |
+
parser.add_argument("--thinking_ratio", type=float, default=0.1, help="按概率开启thinking(0.0~1.0)")
|
| 405 |
+
parser.add_argument("--reward_model_path", type=str, default="../../internlm2-1_8b-reward", help="Reward模型路径")
|
| 406 |
+
parser.add_argument("--rollout_engine", type=str, default="torch", choices=["torch", "sglang"], help="rollout引擎类型")
|
| 407 |
+
parser.add_argument("--sglang_base_url", type=str, default="http://localhost:8998", help="SGLang服务器URL")
|
| 408 |
+
parser.add_argument("--sglang_model_path", type=str, default="../model", help="SGLang tokenizer路径")
|
| 409 |
+
parser.add_argument("--sglang_shared_path", type=str, default="./sglang_ckpt_agent", help="SGLang共享存储路径")
|
| 410 |
+
args = parser.parse_args()
|
| 411 |
+
|
| 412 |
+
local_rank = init_distributed_mode()
|
| 413 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 414 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 415 |
+
|
| 416 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 417 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers,
|
| 418 |
+
max_seq_len=args.max_seq_len + args.max_gen_len, use_moe=bool(args.use_moe))
|
| 419 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume == 1 else None
|
| 420 |
+
|
| 421 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 422 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 423 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 424 |
+
|
| 425 |
+
wandb = None
|
| 426 |
+
if args.use_wandb and is_main_process():
|
| 427 |
+
import swanlab as wandb
|
| 428 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 429 |
+
resume = 'must' if wandb_id else None
|
| 430 |
+
wandb.init(project=args.wandb_project, name=f"Agent-RL-E{args.epochs}-B{args.batch_size}-LR{args.learning_rate}", id=wandb_id, resume=resume)
|
| 431 |
+
|
| 432 |
+
model, tokenizer = init_model(lm_config, args.from_weight, device=args.device)
|
| 433 |
+
|
| 434 |
+
ref_model, _ = init_model(lm_config, args.from_weight, device=args.device)
|
| 435 |
+
ref_model = ref_model.eval().requires_grad_(False)
|
| 436 |
+
|
| 437 |
+
reward_model = LMForRewardModel(args.reward_model_path, device=args.device, dtype=torch.float16)
|
| 438 |
+
Logger(f'Loaded reward model from {args.reward_model_path}')
|
| 439 |
+
# Rollout引擎
|
| 440 |
+
rollout_engine = create_rollout_engine(
|
| 441 |
+
engine_type=args.rollout_engine,
|
| 442 |
+
policy_model=model,
|
| 443 |
+
tokenizer=tokenizer,
|
| 444 |
+
device=args.device,
|
| 445 |
+
autocast_ctx=autocast_ctx,
|
| 446 |
+
sglang_base_url=args.sglang_base_url,
|
| 447 |
+
sglang_model_path=args.sglang_model_path,
|
| 448 |
+
sglang_shared_path=args.sglang_shared_path,
|
| 449 |
+
)
|
| 450 |
+
train_ds = AgentRLDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)
|
| 451 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 452 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 453 |
+
def collate_fn(batch): return {'messages': [b['messages'] for b in batch], 'tools': [b['tools'] for b in batch], 'gt': [b['gt'] for b in batch]}
|
| 454 |
+
loader_for_count = DataLoader(train_ds, batch_size=args.batch_size, sampler=train_sampler, collate_fn=collate_fn)
|
| 455 |
+
iters = len(loader_for_count)
|
| 456 |
+
total_optimizer_steps = math.ceil(iters / args.accumulation_steps) * args.epochs
|
| 457 |
+
scheduler = CosineAnnealingLR(optimizer, T_max=total_optimizer_steps, eta_min=args.learning_rate / 10)
|
| 458 |
+
|
| 459 |
+
start_epoch, start_step = 0, 0
|
| 460 |
+
if ckp_data:
|
| 461 |
+
model.load_state_dict(ckp_data['model'])
|
| 462 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 463 |
+
scheduler.load_state_dict(ckp_data['scheduler'])
|
| 464 |
+
start_epoch = ckp_data['epoch']
|
| 465 |
+
start_step = ckp_data.get('step', 0)
|
| 466 |
+
|
| 467 |
+
if args.use_compile == 1:
|
| 468 |
+
model = torch.compile(model)
|
| 469 |
+
Logger('torch.compile enabled')
|
| 470 |
+
rollout_engine.update_policy(model)
|
| 471 |
+
if dist.is_initialized():
|
| 472 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 473 |
+
rollout_engine.update_policy(model)
|
| 474 |
+
|
| 475 |
+
for epoch in range(start_epoch, args.epochs):
|
| 476 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 477 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 478 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 479 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 480 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True, collate_fn=collate_fn)
|
| 481 |
+
if skip > 0:
|
| 482 |
+
Logger(f'Epoch [{epoch+1}/{args.epochs}]: skip {start_step} steps')
|
| 483 |
+
rl_train_epoch(epoch, loader, len(loader) + skip, rollout_engine, ref_model, reward_model, start_step, wandb, use_sglang = (args.rollout_engine == "sglang"))
|
| 484 |
+
else:
|
| 485 |
+
rl_train_epoch(epoch, loader, len(loader), rollout_engine, ref_model, reward_model, 0, wandb, use_sglang = (args.rollout_engine == "sglang"))
|
| 486 |
+
|
| 487 |
+
if dist.is_initialized():
|
| 488 |
+
dist.barrier()
|
| 489 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import time
|
| 7 |
+
import warnings
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
from contextlib import nullcontext
|
| 12 |
+
from torch import optim
|
| 13 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 14 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 15 |
+
from omni.models.minimind import MiniMindConfig
|
| 16 |
+
from omni.datasets.lm_dataset import SFTDataset
|
| 17 |
+
from omni.utils.training import get_lr, Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, init_model, SkipBatchSampler
|
| 18 |
+
|
| 19 |
+
warnings.filterwarnings('ignore')
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def distillation_loss(student_logits, teacher_logits, temperature=1.0, reduction='batchmean'):
|
| 23 |
+
with torch.no_grad():
|
| 24 |
+
teacher_probs = F.softmax(teacher_logits / temperature, dim=-1).detach()
|
| 25 |
+
|
| 26 |
+
student_log_probs = F.log_softmax(student_logits / temperature, dim=-1)
|
| 27 |
+
|
| 28 |
+
kl = F.kl_div(
|
| 29 |
+
student_log_probs,
|
| 30 |
+
teacher_probs,
|
| 31 |
+
reduction=reduction
|
| 32 |
+
)
|
| 33 |
+
return (temperature ** 2) * kl
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def train_epoch(epoch, loader, iters, teacher_model, lm_config_student, start_step=0, wandb=None, alpha=0.0, temperature=1.0):
|
| 37 |
+
start_time = time.time()
|
| 38 |
+
last_step = start_step
|
| 39 |
+
|
| 40 |
+
if teacher_model is not None:
|
| 41 |
+
teacher_model.eval()
|
| 42 |
+
teacher_model.requires_grad_(False)
|
| 43 |
+
|
| 44 |
+
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
| 45 |
+
last_step = step
|
| 46 |
+
input_ids = input_ids.to(args.device)
|
| 47 |
+
labels = labels.to(args.device)
|
| 48 |
+
loss_mask = (labels[..., 1:] != -100).float()
|
| 49 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 50 |
+
for param_group in optimizer.param_groups:
|
| 51 |
+
param_group['lr'] = lr
|
| 52 |
+
|
| 53 |
+
# 前向传播(学生模型)
|
| 54 |
+
with autocast_ctx:
|
| 55 |
+
res = model(input_ids)
|
| 56 |
+
student_logits = res.logits[..., :-1, :].contiguous()
|
| 57 |
+
|
| 58 |
+
# 教师模型前向传播(只在eval & no_grad)
|
| 59 |
+
if teacher_model is not None:
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
teacher_logits = teacher_model(input_ids).logits[..., :-1, :].contiguous()
|
| 62 |
+
vocab_size_student = student_logits.size(-1)
|
| 63 |
+
teacher_logits = teacher_logits[..., :vocab_size_student]
|
| 64 |
+
|
| 65 |
+
# ========== 计算损失 ==========
|
| 66 |
+
# 1) Ground-Truth CE Loss
|
| 67 |
+
shift_labels = labels[..., 1:].contiguous()
|
| 68 |
+
loss_mask_flat = loss_mask.view(-1)
|
| 69 |
+
ce_loss = F.cross_entropy(
|
| 70 |
+
student_logits.view(-1, student_logits.size(-1)),
|
| 71 |
+
shift_labels.view(-1),
|
| 72 |
+
ignore_index=-100,
|
| 73 |
+
reduction='none'
|
| 74 |
+
)
|
| 75 |
+
ce_loss_raw = torch.sum(ce_loss * loss_mask_flat) / (loss_mask_flat.sum() + 1e-8)
|
| 76 |
+
if lm_config_student.use_moe: ce_loss = ce_loss_raw + res.aux_loss
|
| 77 |
+
else: ce_loss = ce_loss_raw
|
| 78 |
+
|
| 79 |
+
# 2) Distillation Loss
|
| 80 |
+
if teacher_model is not None:
|
| 81 |
+
distill_loss = distillation_loss(
|
| 82 |
+
student_logits.view(-1, student_logits.size(-1))[loss_mask_flat == 1],
|
| 83 |
+
teacher_logits.view(-1, teacher_logits.size(-1))[loss_mask_flat == 1],
|
| 84 |
+
temperature=temperature
|
| 85 |
+
)
|
| 86 |
+
else:
|
| 87 |
+
distill_loss = torch.tensor(0.0, device=args.device)
|
| 88 |
+
|
| 89 |
+
# 3) 总损失 = alpha * CE + (1-alpha) * Distill
|
| 90 |
+
loss = (alpha * ce_loss + (1 - alpha) * distill_loss) / args.accumulation_steps
|
| 91 |
+
|
| 92 |
+
scaler.scale(loss).backward()
|
| 93 |
+
|
| 94 |
+
if step % args.accumulation_steps == 0:
|
| 95 |
+
scaler.unscale_(optimizer)
|
| 96 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 97 |
+
scaler.step(optimizer)
|
| 98 |
+
scaler.update()
|
| 99 |
+
optimizer.zero_grad(set_to_none=True)
|
| 100 |
+
|
| 101 |
+
if step % args.log_interval == 0 or step == iters:
|
| 102 |
+
spend_time = time.time() - start_time
|
| 103 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 104 |
+
current_ce_loss = ce_loss_raw.item()
|
| 105 |
+
current_aux_loss = res.aux_loss.item() if lm_config_student.use_moe else 0.0
|
| 106 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 107 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 108 |
+
|
| 109 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, ce: {current_ce_loss:.4f}, aux_loss: {current_aux_loss:.4f}, distill: {distill_loss.item():.4f}, learning_rate: {current_lr:.8f}, epoch_time: {eta_min:.3f}min')
|
| 110 |
+
|
| 111 |
+
if wandb:
|
| 112 |
+
wandb.log({
|
| 113 |
+
"loss": current_loss,
|
| 114 |
+
"ce_loss": current_ce_loss,
|
| 115 |
+
"aux_loss": current_aux_loss,
|
| 116 |
+
"distill_loss": distill_loss.item() if teacher_model is not None else 0.0,
|
| 117 |
+
"learning_rate": current_lr,
|
| 118 |
+
"epoch_time": eta_min
|
| 119 |
+
})
|
| 120 |
+
|
| 121 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 122 |
+
model.eval()
|
| 123 |
+
moe_suffix = '_moe' if lm_config_student.use_moe else ''
|
| 124 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config_student.hidden_size}{moe_suffix}.pth'
|
| 125 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 126 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 127 |
+
state_dict = raw_model.state_dict()
|
| 128 |
+
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
|
| 129 |
+
lm_checkpoint(lm_config_student, weight=args.save_weight, model=model, optimizer=optimizer, scaler=scaler, epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints')
|
| 130 |
+
model.train()
|
| 131 |
+
del state_dict
|
| 132 |
+
|
| 133 |
+
del input_ids, labels, loss_mask, res, student_logits, ce_loss, distill_loss, loss
|
| 134 |
+
|
| 135 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 136 |
+
scaler.unscale_(optimizer)
|
| 137 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 138 |
+
scaler.step(optimizer)
|
| 139 |
+
scaler.update()
|
| 140 |
+
optimizer.zero_grad(set_to_none=True)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
if __name__ == "__main__":
|
| 144 |
+
# 模拟用moe模型蒸馏dense模型,也可以用更大teacher_hidden_size模型蒸馏更小student_hidden_size的
|
| 145 |
+
parser = argparse.ArgumentParser(description="MiniMind Knowledge Distillation")
|
| 146 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 147 |
+
parser.add_argument('--save_weight', default='full_dist', type=str, help="保存权重的前缀名")
|
| 148 |
+
parser.add_argument("--epochs", type=int, default=6, help="训练轮数")
|
| 149 |
+
parser.add_argument("--batch_size", type=int, default=32, help="batch size")
|
| 150 |
+
parser.add_argument("--learning_rate", type=float, default=5e-6, help="初始学习率")
|
| 151 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 152 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 153 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 154 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 155 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 156 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 157 |
+
parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔")
|
| 158 |
+
parser.add_argument("--max_seq_len", type=int, default=340, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
|
| 159 |
+
parser.add_argument("--data_path", type=str, default="../dataset/sft_t2t_mini.jsonl", help="训练数据路径")
|
| 160 |
+
parser.add_argument('--student_hidden_size', default=768, type=int, help="学生模型隐藏层维度")
|
| 161 |
+
parser.add_argument('--student_num_layers', default=8, type=int, help="学生模型隐藏层数量")
|
| 162 |
+
parser.add_argument('--teacher_hidden_size', default=768, type=int, help="教师模型隐藏层维度")
|
| 163 |
+
parser.add_argument('--teacher_num_layers', default=8, type=int, help="教师模型隐藏层数量")
|
| 164 |
+
parser.add_argument('--student_use_moe', default=0, type=int, choices=[0, 1], help="学生模型是否使用MoE(0=否,1=是)")
|
| 165 |
+
parser.add_argument('--teacher_use_moe', default=1, type=int, choices=[0, 1], help="教师模型是否使用MoE(0=否,1=是)")
|
| 166 |
+
parser.add_argument('--from_student_weight', default='full_sft', type=str, help="学生模型基于哪个权重")
|
| 167 |
+
parser.add_argument('--from_teacher_weight', default='full_sft', type=str, help="教师模型基于哪个权重")
|
| 168 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 169 |
+
parser.add_argument('--alpha', default=0.5, type=float, help="CE损失权重,总损失=alpha*CE+(1-alpha)*KL")
|
| 170 |
+
parser.add_argument('--temperature', default=1.5, type=float, help="蒸馏温度(推荐范围1.0-2.0)")
|
| 171 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 172 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-Distillation", help="wandb项目名")
|
| 173 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 174 |
+
args = parser.parse_args()
|
| 175 |
+
|
| 176 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 177 |
+
local_rank = init_distributed_mode()
|
| 178 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 179 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 180 |
+
|
| 181 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 182 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 183 |
+
lm_config_student = MiniMindConfig(hidden_size=args.student_hidden_size, num_hidden_layers=args.student_num_layers, use_moe=bool(args.student_use_moe))
|
| 184 |
+
lm_config_teacher = MiniMindConfig(hidden_size=args.teacher_hidden_size, num_hidden_layers=args.teacher_num_layers, use_moe=bool(args.teacher_use_moe))
|
| 185 |
+
ckp_data = lm_checkpoint(lm_config_student, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 186 |
+
|
| 187 |
+
# ========== 3. 设置混合精度 ==========
|
| 188 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 189 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 190 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 191 |
+
|
| 192 |
+
# ========== 4. 配wandb ==========
|
| 193 |
+
wandb = None
|
| 194 |
+
if args.use_wandb and is_main_process():
|
| 195 |
+
import swanlab as wandb
|
| 196 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 197 |
+
resume = 'must' if wandb_id else None
|
| 198 |
+
wandb_run_name = f"MiniMind-Distill-S{args.student_hidden_size}T{args.teacher_hidden_size}-Epoch-{args.epochs}-BS-{args.batch_size}-LR-{args.learning_rate}"
|
| 199 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 200 |
+
|
| 201 |
+
# ========== 5. 定义学生和教师模型 ==========
|
| 202 |
+
model, tokenizer = init_model(lm_config_student, args.from_student_weight, device=args.device)
|
| 203 |
+
Logger(f'学生模型总参数量:{sum(p.numel() for p in model.parameters()) / 1e6:.3f} M')
|
| 204 |
+
teacher_model, _ = init_model(lm_config_teacher, args.from_teacher_weight, device=args.device)
|
| 205 |
+
teacher_model.eval()
|
| 206 |
+
teacher_model.requires_grad_(False)
|
| 207 |
+
Logger(f'教师模型总参数量:{sum(p.numel() for p in teacher_model.parameters()) / 1e6:.3f} M')
|
| 208 |
+
train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
| 209 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 210 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 211 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 212 |
+
|
| 213 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 214 |
+
start_epoch, start_step = 0, 0
|
| 215 |
+
if ckp_data:
|
| 216 |
+
model.load_state_dict(ckp_data['model'])
|
| 217 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 218 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 219 |
+
start_epoch = ckp_data['epoch']
|
| 220 |
+
start_step = ckp_data.get('step', 0)
|
| 221 |
+
|
| 222 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 223 |
+
if args.use_compile == 1:
|
| 224 |
+
model = torch.compile(model)
|
| 225 |
+
Logger('torch.compile enabled')
|
| 226 |
+
if dist.is_initialized():
|
| 227 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 228 |
+
|
| 229 |
+
# ========== 8. 开始训练 ==========
|
| 230 |
+
for epoch in range(start_epoch, args.epochs):
|
| 231 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 232 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 233 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 234 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 235 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 236 |
+
if skip > 0:
|
| 237 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 238 |
+
train_epoch(epoch, loader, len(loader) + skip, teacher_model, lm_config_student, start_step, wandb, args.alpha, args.temperature)
|
| 239 |
+
else:
|
| 240 |
+
train_epoch(epoch, loader, len(loader), teacher_model, lm_config_student, 0, wandb, args.alpha, args.temperature)
|
| 241 |
+
|
| 242 |
+
# ========== 9. 清理分布进程 ==========
|
| 243 |
+
if dist.is_initialized():
|
| 244 |
+
dist.barrier()
|
| 245 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import time
|
| 7 |
+
import warnings
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
from contextlib import nullcontext
|
| 12 |
+
from torch import optim
|
| 13 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 14 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 15 |
+
from omni.models.minimind import MiniMindConfig
|
| 16 |
+
from omni.datasets.lm_dataset import DPODataset
|
| 17 |
+
from omni.utils.training import get_lr, Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, init_model, SkipBatchSampler
|
| 18 |
+
|
| 19 |
+
warnings.filterwarnings('ignore')
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def logits_to_log_probs(logits, labels):
|
| 23 |
+
# logits shape: (batch_size, seq_len, vocab_size)
|
| 24 |
+
# labels shape: (batch_size, seq_len)
|
| 25 |
+
# log_probs shape: (batch_size, seq_len)
|
| 26 |
+
log_probs = F.log_softmax(logits, dim=2)
|
| 27 |
+
log_probs_per_token = torch.gather(log_probs, dim=2, index=labels.unsqueeze(2)).squeeze(-1)
|
| 28 |
+
return log_probs_per_token
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def dpo_loss(ref_log_probs, policy_log_probs, mask, beta):
|
| 32 |
+
# ref_log_probs 和 policy_log_probs 都是 shape: (batch_size, seq_len)
|
| 33 |
+
ref_log_probs = (ref_log_probs * mask).sum(dim=1)
|
| 34 |
+
policy_log_probs = (policy_log_probs * mask).sum(dim=1)
|
| 35 |
+
|
| 36 |
+
# 将 chosen 和 rejected 数据分开
|
| 37 |
+
batch_size = ref_log_probs.shape[0]
|
| 38 |
+
chosen_ref_log_probs = ref_log_probs[:batch_size // 2]
|
| 39 |
+
reject_ref_log_probs = ref_log_probs[batch_size // 2:]
|
| 40 |
+
chosen_policy_log_probs = policy_log_probs[:batch_size // 2]
|
| 41 |
+
reject_policy_log_probs = policy_log_probs[batch_size // 2:]
|
| 42 |
+
|
| 43 |
+
pi_logratios = chosen_policy_log_probs - reject_policy_log_probs
|
| 44 |
+
ref_logratios = chosen_ref_log_probs - reject_ref_log_probs
|
| 45 |
+
logits = pi_logratios - ref_logratios
|
| 46 |
+
loss = -F.logsigmoid(beta * logits)
|
| 47 |
+
return loss.mean()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def train_epoch(epoch, loader, iters, ref_model, lm_config, start_step=0, wandb=None, beta=0.1):
|
| 51 |
+
start_time = time.time()
|
| 52 |
+
last_step = start_step
|
| 53 |
+
|
| 54 |
+
for step, batch in enumerate(loader, start=start_step + 1):
|
| 55 |
+
last_step = step
|
| 56 |
+
x_chosen = batch['x_chosen'].to(args.device)
|
| 57 |
+
x_rejected = batch['x_rejected'].to(args.device)
|
| 58 |
+
y_chosen = batch['y_chosen'].to(args.device)
|
| 59 |
+
y_rejected = batch['y_rejected'].to(args.device)
|
| 60 |
+
mask_chosen = batch['mask_chosen'].to(args.device)
|
| 61 |
+
mask_rejected = batch['mask_rejected'].to(args.device)
|
| 62 |
+
x = torch.cat([x_chosen, x_rejected], dim=0)
|
| 63 |
+
y = torch.cat([y_chosen, y_rejected], dim=0)
|
| 64 |
+
mask = torch.cat([mask_chosen, mask_rejected], dim=0)
|
| 65 |
+
|
| 66 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 67 |
+
for param_group in optimizer.param_groups:
|
| 68 |
+
param_group['lr'] = lr
|
| 69 |
+
|
| 70 |
+
with autocast_ctx:
|
| 71 |
+
with torch.no_grad():
|
| 72 |
+
ref_outputs = ref_model(x)
|
| 73 |
+
ref_logits = ref_outputs.logits
|
| 74 |
+
ref_log_probs = logits_to_log_probs(ref_logits, y)
|
| 75 |
+
|
| 76 |
+
outputs = model(x)
|
| 77 |
+
logits = outputs.logits
|
| 78 |
+
policy_log_probs = logits_to_log_probs(logits, y)
|
| 79 |
+
|
| 80 |
+
dpo_loss_val = dpo_loss(ref_log_probs, policy_log_probs, mask, beta=beta)
|
| 81 |
+
loss = dpo_loss_val + outputs.aux_loss
|
| 82 |
+
loss = loss / args.accumulation_steps
|
| 83 |
+
|
| 84 |
+
scaler.scale(loss).backward()
|
| 85 |
+
|
| 86 |
+
if step % args.accumulation_steps == 0:
|
| 87 |
+
scaler.unscale_(optimizer)
|
| 88 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 89 |
+
scaler.step(optimizer)
|
| 90 |
+
scaler.update()
|
| 91 |
+
optimizer.zero_grad(set_to_none=True)
|
| 92 |
+
|
| 93 |
+
if step % args.log_interval == 0 or step == iters:
|
| 94 |
+
spend_time = time.time() - start_time
|
| 95 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 96 |
+
current_dpo_loss = dpo_loss_val.item()
|
| 97 |
+
current_aux_loss = outputs.aux_loss.item()
|
| 98 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 99 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 100 |
+
|
| 101 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, dpo_loss: {current_dpo_loss:.4f}, aux_loss: {current_aux_loss:.4f}, learning_rate: {current_lr:.8f}, epoch_time: {eta_min:.3f}min')
|
| 102 |
+
|
| 103 |
+
if wandb: wandb.log({"loss": current_loss, "dpo_loss": current_dpo_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})
|
| 104 |
+
|
| 105 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 106 |
+
model.eval()
|
| 107 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 108 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 109 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 110 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 111 |
+
state_dict = raw_model.state_dict()
|
| 112 |
+
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
|
| 113 |
+
lm_checkpoint(lm_config, weight=args.save_weight, model=model, optimizer=optimizer, scaler=scaler, epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints')
|
| 114 |
+
model.train()
|
| 115 |
+
del state_dict
|
| 116 |
+
|
| 117 |
+
del x_chosen, x_rejected, y_chosen, y_rejected, mask_chosen, mask_rejected, x, y, mask
|
| 118 |
+
del ref_outputs, ref_logits, ref_log_probs, outputs, logits, policy_log_probs, loss
|
| 119 |
+
|
| 120 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 121 |
+
scaler.unscale_(optimizer)
|
| 122 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 123 |
+
scaler.step(optimizer)
|
| 124 |
+
scaler.update()
|
| 125 |
+
optimizer.zero_grad(set_to_none=True)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
if __name__ == "__main__":
|
| 129 |
+
parser = argparse.ArgumentParser(description="MiniMind DPO (Direct Preference Optimization)")
|
| 130 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 131 |
+
parser.add_argument('--save_weight', default='dpo', type=str, help="保存权重的前缀名")
|
| 132 |
+
parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
|
| 133 |
+
parser.add_argument("--batch_size", type=int, default=4, help="batch size")
|
| 134 |
+
parser.add_argument("--learning_rate", type=float, default=4e-8, help="初始学习率(建议<=5e-8避免遗忘)")
|
| 135 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 136 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 137 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 138 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 139 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 140 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 141 |
+
parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔")
|
| 142 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 143 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 144 |
+
parser.add_argument('--max_seq_len', default=1024, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
|
| 145 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 146 |
+
parser.add_argument("--data_path", type=str, default="../dataset/dpo.jsonl", help="DPO训练数据路径")
|
| 147 |
+
parser.add_argument('--from_weight', default='full_sft', type=str, help="基于哪个权重训练")
|
| 148 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 149 |
+
parser.add_argument('--beta', default=0.15, type=float, help="DPO中的beta参数")
|
| 150 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 151 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-DPO", help="wandb项目名")
|
| 152 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 153 |
+
args = parser.parse_args()
|
| 154 |
+
|
| 155 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 156 |
+
local_rank = init_distributed_mode()
|
| 157 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 158 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 159 |
+
|
| 160 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 161 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 162 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe))
|
| 163 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 164 |
+
|
| 165 |
+
# ========== 3. 设置混合精度 ==========
|
| 166 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 167 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 168 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 169 |
+
|
| 170 |
+
# ========== 4. 配wandb ==========
|
| 171 |
+
wandb = None
|
| 172 |
+
if args.use_wandb and is_main_process():
|
| 173 |
+
import swanlab as wandb
|
| 174 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 175 |
+
resume = 'must' if wandb_id else None
|
| 176 |
+
wandb_run_name = f"MiniMind-DPO-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LR-{args.learning_rate}"
|
| 177 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 178 |
+
|
| 179 |
+
# ========== 5. 定义模型和参考模型 ==========
|
| 180 |
+
model, tokenizer = init_model(lm_config, args.from_weight, device=args.device)
|
| 181 |
+
Logger(f'策略模型总参数量:{sum(p.numel() for p in model.parameters()) / 1e6:.3f} M')
|
| 182 |
+
# 初始化参考模型(ref_model冻结)
|
| 183 |
+
ref_model, _ = init_model(lm_config, args.from_weight, device=args.device)
|
| 184 |
+
ref_model.eval()
|
| 185 |
+
ref_model.requires_grad_(False)
|
| 186 |
+
Logger(f'参考模型总参数量:{sum(p.numel() for p in ref_model.parameters()) / 1e6:.3f} M')
|
| 187 |
+
|
| 188 |
+
train_ds = DPODataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
| 189 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 190 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 191 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 192 |
+
|
| 193 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 194 |
+
start_epoch, start_step = 0, 0
|
| 195 |
+
if ckp_data:
|
| 196 |
+
model.load_state_dict(ckp_data['model'])
|
| 197 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 198 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 199 |
+
start_epoch = ckp_data['epoch']
|
| 200 |
+
start_step = ckp_data.get('step', 0)
|
| 201 |
+
|
| 202 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 203 |
+
if args.use_compile == 1:
|
| 204 |
+
model = torch.compile(model)
|
| 205 |
+
Logger('torch.compile enabled')
|
| 206 |
+
if dist.is_initialized():
|
| 207 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 208 |
+
|
| 209 |
+
# ========== 8. 开始训练 ==========
|
| 210 |
+
for epoch in range(start_epoch, args.epochs):
|
| 211 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 212 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 213 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 214 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 215 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 216 |
+
if skip > 0:
|
| 217 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 218 |
+
train_epoch(epoch, loader, len(loader) + skip, ref_model, lm_config, start_step, wandb, args.beta)
|
| 219 |
+
else:
|
| 220 |
+
train_epoch(epoch, loader, len(loader), ref_model, lm_config, 0, wandb, args.beta)
|
| 221 |
+
|
| 222 |
+
# ========== 9. 清理分布进程 ==========
|
| 223 |
+
if dist.is_initialized():
|
| 224 |
+
dist.barrier()
|
| 225 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import time
|
| 7 |
+
import warnings
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from contextlib import nullcontext
|
| 11 |
+
from torch import optim, nn
|
| 12 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 13 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 14 |
+
from omni.models.minimind import MiniMindConfig
|
| 15 |
+
from omni.datasets.lm_dataset import SFTDataset
|
| 16 |
+
from omni.utils.training import get_lr, Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, init_model, SkipBatchSampler
|
| 17 |
+
|
| 18 |
+
warnings.filterwarnings('ignore')
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def train_epoch(epoch, loader, iters, start_step=0, wandb=None):
|
| 22 |
+
start_time = time.time()
|
| 23 |
+
last_step = start_step
|
| 24 |
+
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
| 25 |
+
input_ids = input_ids.to(args.device)
|
| 26 |
+
labels = labels.to(args.device)
|
| 27 |
+
last_step = step
|
| 28 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 29 |
+
for param_group in optimizer.param_groups:
|
| 30 |
+
param_group['lr'] = lr
|
| 31 |
+
|
| 32 |
+
with autocast_ctx:
|
| 33 |
+
res = model(input_ids, labels=labels)
|
| 34 |
+
loss = res.loss + res.aux_loss
|
| 35 |
+
loss = loss / args.accumulation_steps
|
| 36 |
+
|
| 37 |
+
scaler.scale(loss).backward()
|
| 38 |
+
|
| 39 |
+
if step % args.accumulation_steps == 0:
|
| 40 |
+
scaler.unscale_(optimizer)
|
| 41 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 42 |
+
|
| 43 |
+
scaler.step(optimizer)
|
| 44 |
+
scaler.update()
|
| 45 |
+
|
| 46 |
+
optimizer.zero_grad(set_to_none=True)
|
| 47 |
+
|
| 48 |
+
if step % args.log_interval == 0 or step == iters:
|
| 49 |
+
spend_time = time.time() - start_time
|
| 50 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 51 |
+
current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0
|
| 52 |
+
current_logits_loss = current_loss - current_aux_loss
|
| 53 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 54 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 55 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, logits_loss: {current_logits_loss:.4f}, aux_loss: {current_aux_loss:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')
|
| 56 |
+
if wandb: wandb.log({"loss": current_loss, "logits_loss": current_logits_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})
|
| 57 |
+
|
| 58 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 59 |
+
model.eval()
|
| 60 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 61 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 62 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 63 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 64 |
+
state_dict = raw_model.state_dict()
|
| 65 |
+
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
|
| 66 |
+
lm_checkpoint(lm_config, weight=args.save_weight, model=model, optimizer=optimizer,
|
| 67 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints', scaler=scaler)
|
| 68 |
+
model.train()
|
| 69 |
+
del state_dict
|
| 70 |
+
|
| 71 |
+
del input_ids, labels, res, loss
|
| 72 |
+
|
| 73 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 74 |
+
scaler.unscale_(optimizer)
|
| 75 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 76 |
+
scaler.step(optimizer)
|
| 77 |
+
scaler.update()
|
| 78 |
+
optimizer.zero_grad(set_to_none=True)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
parser = argparse.ArgumentParser(description="MiniMind Full SFT")
|
| 83 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 84 |
+
parser.add_argument('--save_weight', default='full_sft', type=str, help="保存权重的前缀名")
|
| 85 |
+
parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
|
| 86 |
+
parser.add_argument("--batch_size", type=int, default=16, help="batch size")
|
| 87 |
+
parser.add_argument("--learning_rate", type=float, default=1e-5, help="初始学习率")
|
| 88 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 89 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 90 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 91 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 92 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 93 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 94 |
+
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
|
| 95 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 96 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 97 |
+
parser.add_argument('--max_seq_len', default=768, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
|
| 98 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 99 |
+
parser.add_argument("--data_path", type=str, default="../dataset/sft_t2t_mini.jsonl", help="训练数据路径")
|
| 100 |
+
parser.add_argument('--from_weight', default='pretrain', type=str, help="基于哪个权重训练,为none则不基于任何权重训练")
|
| 101 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 102 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 103 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT", help="wandb项目名")
|
| 104 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 105 |
+
args = parser.parse_args()
|
| 106 |
+
|
| 107 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 108 |
+
local_rank = init_distributed_mode()
|
| 109 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 110 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 111 |
+
|
| 112 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 113 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 114 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe))
|
| 115 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 116 |
+
|
| 117 |
+
# ========== 3. 设置混合精度 ==========
|
| 118 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 119 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 120 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 121 |
+
|
| 122 |
+
# ========== 4. 配wandb ==========
|
| 123 |
+
wandb = None
|
| 124 |
+
if args.use_wandb and is_main_process():
|
| 125 |
+
import swanlab as wandb
|
| 126 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 127 |
+
resume = 'must' if wandb_id else None
|
| 128 |
+
wandb_run_name = f"MiniMind-Full-SFT-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"
|
| 129 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 130 |
+
|
| 131 |
+
# ========== 5. 定义模型、数据、优化器 ==========
|
| 132 |
+
model, tokenizer = init_model(lm_config, args.from_weight, device=args.device)
|
| 133 |
+
train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
| 134 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 135 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 136 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 137 |
+
|
| 138 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 139 |
+
start_epoch, start_step = 0, 0
|
| 140 |
+
if ckp_data:
|
| 141 |
+
model.load_state_dict(ckp_data['model'])
|
| 142 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 143 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 144 |
+
start_epoch = ckp_data['epoch']
|
| 145 |
+
start_step = ckp_data.get('step', 0)
|
| 146 |
+
|
| 147 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 148 |
+
if args.use_compile == 1:
|
| 149 |
+
model = torch.compile(model)
|
| 150 |
+
Logger('torch.compile enabled')
|
| 151 |
+
if dist.is_initialized():
|
| 152 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 153 |
+
|
| 154 |
+
# ========== 8. 开始训练 ==========
|
| 155 |
+
for epoch in range(start_epoch, args.epochs):
|
| 156 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 157 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 158 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 159 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 160 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 161 |
+
if skip > 0:
|
| 162 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 163 |
+
train_epoch(epoch, loader, len(loader) + skip, start_step, wandb)
|
| 164 |
+
else:
|
| 165 |
+
train_epoch(epoch, loader, len(loader), 0, wandb)
|
| 166 |
+
|
| 167 |
+
# ========== 9. 清理分布进程 ==========
|
| 168 |
+
if dist.is_initialized():
|
| 169 |
+
dist.barrier()
|
| 170 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import time
|
| 4 |
+
import warnings
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.distributed as dist
|
| 8 |
+
from contextlib import nullcontext
|
| 9 |
+
from torch import optim
|
| 10 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 11 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 12 |
+
from omni.models import OmniConfig, MiniMindOmni
|
| 13 |
+
from omni.datasets.lm_dataset import OmniDataset
|
| 14 |
+
from omni.utils import get_lr, Logger, is_main_process, init_distributed_mode, setup_seed, init_omni_model, omni_checkpoint, SkipBatchSampler, log_model_params
|
| 15 |
+
|
| 16 |
+
warnings.filterwarnings('ignore')
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def omni_collate_fn(batch):
|
| 20 |
+
"""自定义collate函数,处理变长audio_inputs和pixel_values"""
|
| 21 |
+
input_ids, labels, audio_labels, audio_inputs, audio_lens, pixel_values, spk_emb = zip(*batch)
|
| 22 |
+
input_ids = torch.stack(input_ids)
|
| 23 |
+
labels = torch.stack(labels)
|
| 24 |
+
audio_labels = torch.stack(audio_labels)
|
| 25 |
+
audio_lens = torch.tensor(audio_lens, dtype=torch.long)
|
| 26 |
+
valid_audios = [a for a in audio_inputs if a is not None]
|
| 27 |
+
if valid_audios:
|
| 28 |
+
max_t = max(a.size(1) for a in valid_audios)
|
| 29 |
+
padded = [a if a.size(1) == max_t else torch.nn.functional.pad(a, (0, 0, 0, max_t - a.size(1))) for a in valid_audios]
|
| 30 |
+
audio_inputs = torch.cat(padded, dim=0)
|
| 31 |
+
else:
|
| 32 |
+
audio_inputs = None
|
| 33 |
+
valid_images = [p for p in pixel_values if p is not None]
|
| 34 |
+
if valid_images:
|
| 35 |
+
if hasattr(valid_images[0], 'keys'):
|
| 36 |
+
keys = set.intersection(*[set(d.keys()) for d in valid_images])
|
| 37 |
+
pixel_values = {k: torch.cat([d[k] for d in valid_images], dim=0) for k in keys}
|
| 38 |
+
else:
|
| 39 |
+
pixel_values = torch.cat(valid_images, dim=0)
|
| 40 |
+
else:
|
| 41 |
+
pixel_values = None
|
| 42 |
+
spk_emb = torch.stack(spk_emb)
|
| 43 |
+
return input_ids, labels, audio_labels, audio_inputs, audio_lens, pixel_values, spk_emb
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def train_epoch(epoch, loader, iters, start_step=0, wandb=None):
|
| 47 |
+
start_time = time.time()
|
| 48 |
+
last_step = start_step
|
| 49 |
+
for step, (input_ids, labels, audio_labels, audio_inputs, audio_lens, pixel_values, spk_emb) in enumerate(loader, start=start_step + 1):
|
| 50 |
+
input_ids = input_ids.to(args.device)
|
| 51 |
+
labels = labels.to(args.device)
|
| 52 |
+
audio_labels = audio_labels.to(args.device)
|
| 53 |
+
audio_lens = audio_lens.to(args.device)
|
| 54 |
+
if audio_inputs is not None:
|
| 55 |
+
audio_inputs = audio_inputs.to(args.device)
|
| 56 |
+
if pixel_values is not None:
|
| 57 |
+
if hasattr(pixel_values, 'keys'):
|
| 58 |
+
pixel_values = {k: v.to(args.device) for k, v in pixel_values.items()}
|
| 59 |
+
else:
|
| 60 |
+
pixel_values = pixel_values.to(args.device)
|
| 61 |
+
spk_emb = spk_emb.to(args.device)
|
| 62 |
+
last_step = step
|
| 63 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 64 |
+
for param_group in optimizer.param_groups:
|
| 65 |
+
param_group['lr'] = lr
|
| 66 |
+
|
| 67 |
+
with autocast_ctx:
|
| 68 |
+
res = model(input_ids, audio_inputs=audio_inputs, audio_lens=audio_lens, pixel_values=pixel_values, spk_emb=spk_emb)
|
| 69 |
+
loss_fct = nn.CrossEntropyLoss(reduction='none')
|
| 70 |
+
|
| 71 |
+
# Text loss
|
| 72 |
+
text_loss_raw = loss_fct(res.logits.view(-1, res.logits.size(-1)), labels.view(-1))
|
| 73 |
+
text_mask = (labels.view(-1) != -100).float()
|
| 74 |
+
text_loss = (text_loss_raw * text_mask).sum() / (text_mask.sum() + 1e-9)
|
| 75 |
+
|
| 76 |
+
# Audio loss
|
| 77 |
+
audio_loss = res.audio_logits[0].sum() * 0
|
| 78 |
+
for i, al in enumerate(res.audio_logits):
|
| 79 |
+
al_flat = al.view(-1, al.size(-1))
|
| 80 |
+
target_flat = audio_labels[:, i, :].reshape(-1)
|
| 81 |
+
layer_loss = loss_fct(al_flat, target_flat)
|
| 82 |
+
valid_mask = (target_flat != -100).float()
|
| 83 |
+
stop_mask = (target_flat == 2050).float()
|
| 84 |
+
weighted_loss = layer_loss * valid_mask * (1 + stop_mask * 9)
|
| 85 |
+
msum = valid_mask.sum()
|
| 86 |
+
if msum > 0:
|
| 87 |
+
audio_loss = audio_loss + weighted_loss.sum() / (msum + 1e-9)
|
| 88 |
+
audio_loss = audio_loss / 8
|
| 89 |
+
|
| 90 |
+
loss = (text_loss + audio_loss + res.aux_loss) / args.accumulation_steps
|
| 91 |
+
|
| 92 |
+
scaler.scale(loss).backward()
|
| 93 |
+
if step % args.accumulation_steps == 0:
|
| 94 |
+
scaler.unscale_(optimizer)
|
| 95 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 96 |
+
scaler.step(optimizer)
|
| 97 |
+
scaler.update()
|
| 98 |
+
optimizer.zero_grad(set_to_none=True)
|
| 99 |
+
|
| 100 |
+
if step % args.log_interval == 0 or step == iters:
|
| 101 |
+
spend_time = time.time() - start_time
|
| 102 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 103 |
+
text_loss_val = text_loss.item() if isinstance(text_loss, torch.Tensor) else 0
|
| 104 |
+
audio_loss_val = audio_loss.item() if isinstance(audio_loss, torch.Tensor) else 0
|
| 105 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 106 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 107 |
+
Logger(f'Epoch:[{epoch+1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, text: {text_loss_val:.4f}, audio: {audio_loss_val:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')
|
| 108 |
+
if wandb:
|
| 109 |
+
wandb.log({"loss": current_loss, "text_loss": text_loss_val,
|
| 110 |
+
"audio_loss": audio_loss_val, "lr": current_lr, "epoch_time": eta_min})
|
| 111 |
+
|
| 112 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 113 |
+
model.eval()
|
| 114 |
+
moe_suffix = '_moe' if omni_config.use_moe else ''
|
| 115 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{omni_config.hidden_size}{moe_suffix}.pth'
|
| 116 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 117 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 118 |
+
clean_state_dict = {k: v for k, v in raw_model.state_dict().items() if not k.startswith('audio_encoder.')}
|
| 119 |
+
torch.save({k: v.half().cpu() for k, v in clean_state_dict.items()}, ckp)
|
| 120 |
+
omni_checkpoint(omni_config, weight=args.save_weight, model=model, optimizer=optimizer,
|
| 121 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints', scaler=scaler)
|
| 122 |
+
model.train()
|
| 123 |
+
|
| 124 |
+
del input_ids, labels, audio_labels, audio_inputs, audio_lens, pixel_values, spk_emb, res, loss
|
| 125 |
+
|
| 126 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 127 |
+
scaler.unscale_(optimizer)
|
| 128 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 129 |
+
scaler.step(optimizer)
|
| 130 |
+
scaler.update()
|
| 131 |
+
optimizer.zero_grad(set_to_none=True)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
parser = argparse.ArgumentParser(description="MiniMind-O SFT")
|
| 136 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 137 |
+
parser.add_argument('--save_weight', default='sft_omni', type=str, help="保存权重的前缀名")
|
| 138 |
+
parser.add_argument("--epochs", type=int, default=15, help="训练轮数")
|
| 139 |
+
parser.add_argument("--batch_size", type=int, default=32, help="batch size")
|
| 140 |
+
parser.add_argument("--learning_rate", type=float, default=5e-4, help="初始学习率")
|
| 141 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 142 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 143 |
+
parser.add_argument("--num_workers", type=int, default=4, help="数据加载线程数")
|
| 144 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 145 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 146 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 147 |
+
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
|
| 148 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 149 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 150 |
+
parser.add_argument('--max_seq_len', default=512, type=int, help="训练的最大截断长度")
|
| 151 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构")
|
| 152 |
+
parser.add_argument("--data_path", type=str, default="../dataset/train_t2a_mini.parquet", help="训练数据路径(parquet格式)")
|
| 153 |
+
parser.add_argument("--audio_encoder_dir", type=str, default="../model/SenseVoiceSmall", help="音频encoder路径(SenseVoice)")
|
| 154 |
+
parser.add_argument("--vision_dir", type=str, default="../model/siglip2-base-p32-256-ve", help="CLIP视觉模型路径")
|
| 155 |
+
parser.add_argument('--from_weight', default='llm', type=str, help="基于哪个权重训练,为none则不基于任何权重训练")
|
| 156 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 157 |
+
parser.add_argument('--freeze_backbone', default='none', type=str, choices=['none', 'all', 'last1'], help="冻结主干模型: none=全量训练, all=只训练audio层, last1=只训练最后1层+audio层")
|
| 158 |
+
parser.add_argument('--mode', default='all', type=str, choices=['all', 'audio_proj', 'vision_proj'], help="训练模式: all=全量训练, audio_proj=只训练audio_proj, vision_proj=只训练vision_proj")
|
| 159 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 160 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-O-SFT", help="wandb项目名")
|
| 161 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 162 |
+
args = parser.parse_args()
|
| 163 |
+
|
| 164 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 165 |
+
local_rank = init_distributed_mode()
|
| 166 |
+
if dist.is_initialized():
|
| 167 |
+
args.device = f"cuda:{local_rank}"
|
| 168 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 169 |
+
|
| 170 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 171 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 172 |
+
omni_config = OmniConfig(
|
| 173 |
+
hidden_size=args.hidden_size,
|
| 174 |
+
num_hidden_layers=args.num_hidden_layers,
|
| 175 |
+
use_moe=bool(args.use_moe)
|
| 176 |
+
)
|
| 177 |
+
ckp_data = omni_checkpoint(omni_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 178 |
+
|
| 179 |
+
# ========== 3. 设置混合精度 ==========
|
| 180 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 181 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 182 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 183 |
+
|
| 184 |
+
# ========== 4. 配wandb ==========
|
| 185 |
+
wandb = None
|
| 186 |
+
if args.use_wandb and is_main_process():
|
| 187 |
+
import swanlab as wandb
|
| 188 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 189 |
+
resume = 'must' if wandb_id else None
|
| 190 |
+
wandb_run_name = f"MiniMind-O-SFT-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LR-{args.learning_rate}"
|
| 191 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 192 |
+
|
| 193 |
+
# ========== 5. 定义模型、数据、优化器 ==========
|
| 194 |
+
model, tokenizer = init_omni_model(omni_config, from_weight=args.from_weight,
|
| 195 |
+
audio_encoder_path=args.audio_encoder_dir,
|
| 196 |
+
vision_model_path=args.vision_dir,
|
| 197 |
+
save_dir=args.save_dir, device=args.device,
|
| 198 |
+
freeze_backbone=args.freeze_backbone, from_resume=args.from_resume)
|
| 199 |
+
|
| 200 |
+
if args.use_compile == 1:
|
| 201 |
+
model = torch.compile(model)
|
| 202 |
+
|
| 203 |
+
if model.audio_encoder is not None: model.audio_encoder.to(args.device)
|
| 204 |
+
if model.vision_encoder is not None: model.vision_encoder.to(args.device)
|
| 205 |
+
|
| 206 |
+
if args.mode == 'audio_proj':
|
| 207 |
+
for p in model.parameters(): p.requires_grad = False
|
| 208 |
+
for p in model.audio_proj.parameters(): p.requires_grad = True
|
| 209 |
+
elif args.mode == 'vision_proj':
|
| 210 |
+
for p in model.parameters(): p.requires_grad = False
|
| 211 |
+
for p in model.vision_proj.parameters(): p.requires_grad = True
|
| 212 |
+
log_model_params(model)
|
| 213 |
+
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6
|
| 214 |
+
Logger(f'Trainable: {trainable:.2f}M | Mode: {args.mode} | Freeze: {args.freeze_backbone} | Compile: {"on" if args.use_compile else "off"}')
|
| 215 |
+
|
| 216 |
+
# scheduled_sampling 现在会自动保护 image/audio token 的连续性
|
| 217 |
+
train_ds = OmniDataset(
|
| 218 |
+
args.data_path,
|
| 219 |
+
tokenizer,
|
| 220 |
+
audio_processor=model.audio_processor,
|
| 221 |
+
vision_processor=model.vision_processor,
|
| 222 |
+
max_length=args.max_seq_len,
|
| 223 |
+
image_token_len=model.config.image_token_len
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 227 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 228 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 229 |
+
|
| 230 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 231 |
+
start_epoch, start_step = 0, 0
|
| 232 |
+
if ckp_data:
|
| 233 |
+
model.load_state_dict(ckp_data['model'], strict=False)
|
| 234 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 235 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 236 |
+
start_epoch = ckp_data['epoch']
|
| 237 |
+
start_step = ckp_data.get('step', 0)
|
| 238 |
+
|
| 239 |
+
# ========== 7. DDP包模型 ==========
|
| 240 |
+
if dist.is_initialized():
|
| 241 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 242 |
+
|
| 243 |
+
# ========== 8. 开始训练 ==========
|
| 244 |
+
for epoch in range(start_epoch, args.epochs):
|
| 245 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 246 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 247 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 248 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 249 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, collate_fn=omni_collate_fn, num_workers=args.num_workers, pin_memory=True)
|
| 250 |
+
if skip > 0:
|
| 251 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 252 |
+
train_epoch(epoch, loader, len(loader) + skip, start_step, wandb)
|
| 253 |
+
else:
|
| 254 |
+
train_epoch(epoch, loader, len(loader), 0, wandb)
|
| 255 |
+
|
| 256 |
+
# ========== 9. 清理分布进程 ==========
|
| 257 |
+
if dist.is_initialized(): dist.destroy_process_group()
|
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
import warnings
|
| 5 |
+
import datasets
|
| 6 |
+
import torch
|
| 7 |
+
import torch.distributed as dist
|
| 8 |
+
from contextlib import nullcontext
|
| 9 |
+
from torch import optim, nn
|
| 10 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 11 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 12 |
+
from transformers import AutoTokenizer
|
| 13 |
+
from omni.models import MiniMindVLM, VLMConfig
|
| 14 |
+
from omni.datasets.lm_dataset import VLMDataset
|
| 15 |
+
from omni.utils import get_lr, Logger, is_main_process, init_distributed_mode, setup_seed, init_vlm_model, vlm_checkpoint, SkipBatchSampler, vlm_collate_fn
|
| 16 |
+
|
| 17 |
+
warnings.filterwarnings('ignore')
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def train_epoch(epoch, loader, iters, start_step=0, wandb=None):
|
| 21 |
+
start_time = time.time()
|
| 22 |
+
last_step = start_step
|
| 23 |
+
for step, (input_ids, labels, pixel_values) in enumerate(loader, start=start_step + 1):
|
| 24 |
+
input_ids = input_ids.to(args.device)
|
| 25 |
+
labels = labels.to(args.device)
|
| 26 |
+
pixel_values = {k: v.to(args.device) for k, v in pixel_values.items()} if isinstance(pixel_values, dict) else pixel_values.to(args.device)
|
| 27 |
+
last_step = step
|
| 28 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 29 |
+
for param_group in optimizer.param_groups:
|
| 30 |
+
param_group['lr'] = lr
|
| 31 |
+
|
| 32 |
+
with autocast_ctx:
|
| 33 |
+
res = model(input_ids, labels=labels, pixel_values=pixel_values)
|
| 34 |
+
loss = res.loss + res.aux_loss
|
| 35 |
+
loss = loss / args.accumulation_steps
|
| 36 |
+
|
| 37 |
+
scaler.scale(loss).backward()
|
| 38 |
+
|
| 39 |
+
if step % args.accumulation_steps == 0:
|
| 40 |
+
scaler.unscale_(optimizer)
|
| 41 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 42 |
+
|
| 43 |
+
scaler.step(optimizer)
|
| 44 |
+
scaler.update()
|
| 45 |
+
|
| 46 |
+
optimizer.zero_grad(set_to_none=True)
|
| 47 |
+
|
| 48 |
+
if step % args.log_interval == 0 or step == iters:
|
| 49 |
+
spend_time = time.time() - start_time
|
| 50 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 51 |
+
current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0
|
| 52 |
+
current_logits_loss = current_loss - current_aux_loss
|
| 53 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 54 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 55 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, logits_loss: {current_logits_loss:.4f}, aux_loss: {current_aux_loss:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')
|
| 56 |
+
if wandb: wandb.log({"loss": current_loss, "logits_loss": current_logits_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})
|
| 57 |
+
|
| 58 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 59 |
+
model.eval()
|
| 60 |
+
moe_suffix = '_moe' if vlm_config.use_moe else ''
|
| 61 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{vlm_config.hidden_size}{moe_suffix}.pth'
|
| 62 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 63 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 64 |
+
state_dict = raw_model.state_dict()
|
| 65 |
+
clean_state_dict = {
|
| 66 |
+
key: value for key, value in state_dict.items() if not key.startswith('vision_encoder.')
|
| 67 |
+
}
|
| 68 |
+
clean_state_dict = {k: v.half().cpu() for k, v in clean_state_dict.items()} # 半精度保存并移到CPU
|
| 69 |
+
torch.save(clean_state_dict, ckp)
|
| 70 |
+
vlm_checkpoint(vlm_config, weight=args.save_weight, model=model, optimizer=optimizer,
|
| 71 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints', scaler=scaler)
|
| 72 |
+
model.train()
|
| 73 |
+
del state_dict, clean_state_dict
|
| 74 |
+
|
| 75 |
+
del input_ids, labels, pixel_values, res, loss
|
| 76 |
+
|
| 77 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 78 |
+
scaler.unscale_(optimizer)
|
| 79 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 80 |
+
scaler.step(optimizer)
|
| 81 |
+
scaler.update()
|
| 82 |
+
optimizer.zero_grad(set_to_none=True)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
parser = argparse.ArgumentParser(description="MiniMind-V SFT")
|
| 87 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 88 |
+
parser.add_argument('--save_weight', default='sft_vlm', type=str, help="保存权重的前缀名")
|
| 89 |
+
parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
|
| 90 |
+
parser.add_argument("--batch_size", type=int, default=4, help="batch size")
|
| 91 |
+
parser.add_argument("--learning_rate", type=float, default=5e-6, help="初始学习率")
|
| 92 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 93 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 94 |
+
parser.add_argument("--num_workers", type=int, default=2, help="数据加载线程数")
|
| 95 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 96 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 97 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 98 |
+
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
|
| 99 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 100 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 101 |
+
parser.add_argument('--max_seq_len', default=768, type=int, help="训练的最大截断长度")
|
| 102 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 103 |
+
parser.add_argument("--data_path", type=str, default="../dataset/sft_i2t.parquet", help="训练数据路径")
|
| 104 |
+
parser.add_argument('--from_weight', default='pretrain_vlm', type=str, help="基于哪个权重训练,为none则不基于任何权重训练")
|
| 105 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 106 |
+
parser.add_argument('--freeze_llm', default=1, type=int, choices=[0, 1, 2], help="冻结策略(0=完全可训练,1=冻结+解冻首尾层,2=完全冻结仅训练proj)")
|
| 107 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 108 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 109 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-V-SFT", help="wandb项目名")
|
| 110 |
+
args = parser.parse_args()
|
| 111 |
+
|
| 112 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 113 |
+
local_rank = init_distributed_mode()
|
| 114 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 115 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 116 |
+
|
| 117 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 118 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 119 |
+
vlm_config = VLMConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, max_seq_len=args.max_seq_len, use_moe=bool(args.use_moe))
|
| 120 |
+
ckp_data = vlm_checkpoint(vlm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 121 |
+
|
| 122 |
+
# ========== 3. 设置混合精度 ==========
|
| 123 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 124 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 125 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 126 |
+
|
| 127 |
+
# ========== 4. 配wandb ==========
|
| 128 |
+
wandb = None
|
| 129 |
+
if args.use_wandb and is_main_process():
|
| 130 |
+
import swanlab as wandb
|
| 131 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 132 |
+
resume = 'must' if wandb_id else None
|
| 133 |
+
wandb_run_name = f"MiniMind-V-SFT-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"
|
| 134 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 135 |
+
|
| 136 |
+
# ========== 5. 定义模型、数据、优化器 ==========
|
| 137 |
+
model, tokenizer = init_vlm_model(vlm_config, from_weight=args.from_weight, device=args.device, freeze_llm=args.freeze_llm)
|
| 138 |
+
preprocess = model.vision_encoder.processor
|
| 139 |
+
train_ds = VLMDataset(args.data_path, tokenizer, preprocess=preprocess, image_special_token=vlm_config.image_special_token, image_token_len=vlm_config.image_token_len, max_length=vlm_config.max_seq_len)
|
| 140 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 141 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 142 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 143 |
+
|
| 144 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 145 |
+
start_epoch, start_step = 0, 0
|
| 146 |
+
if ckp_data:
|
| 147 |
+
model.load_state_dict(ckp_data['model'], strict=False)
|
| 148 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 149 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 150 |
+
start_epoch = ckp_data['epoch']
|
| 151 |
+
start_step = ckp_data.get('step', 0)
|
| 152 |
+
|
| 153 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 154 |
+
if args.use_compile == 1:
|
| 155 |
+
model = torch.compile(model)
|
| 156 |
+
Logger('torch.compile enabled')
|
| 157 |
+
if dist.is_initialized():
|
| 158 |
+
model._ddp_params_and_buffers_to_ignore = {"freqs_cos", "freqs_sin"}
|
| 159 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 160 |
+
|
| 161 |
+
# ========== 8. 开始训练 ==========
|
| 162 |
+
for epoch in range(start_epoch, args.epochs):
|
| 163 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 164 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 165 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 166 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 167 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True, collate_fn=vlm_collate_fn)
|
| 168 |
+
if skip > 0:
|
| 169 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 170 |
+
train_epoch(epoch, loader, len(loader) + skip, start_step, wandb)
|
| 171 |
+
else:
|
| 172 |
+
train_epoch(epoch, loader, len(loader), 0, wandb)
|
| 173 |
+
|
| 174 |
+
# ========== 9. 清理分布进程 ==========
|
| 175 |
+
if dist.is_initialized(): dist.destroy_process_group()
|
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import math
|
| 7 |
+
import re
|
| 8 |
+
import gc
|
| 9 |
+
import warnings
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
import torch.distributed as dist
|
| 13 |
+
from transformers import AutoTokenizer
|
| 14 |
+
from contextlib import nullcontext
|
| 15 |
+
from torch import optim
|
| 16 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 17 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 18 |
+
from torch.optim.lr_scheduler import CosineAnnealingLR
|
| 19 |
+
from transformers import AutoModel
|
| 20 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 21 |
+
from omni.datasets.lm_dataset import RLAIFDataset
|
| 22 |
+
from omni.utils.training import Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, SkipBatchSampler, init_model, LMForRewardModel
|
| 23 |
+
from omni.trainers.rollout_engine import create_rollout_engine
|
| 24 |
+
|
| 25 |
+
warnings.filterwarnings('ignore')
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def rep_penalty(text, n=3, cap=0.5):
|
| 29 |
+
toks = re.findall(r"\w+|[^\w\s]", text.lower())
|
| 30 |
+
grams = [tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)]
|
| 31 |
+
return min(cap, (len(grams) - len(set(grams))) * cap * 2 / len(grams)) if grams else 0.0
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def calculate_rewards(prompts, responses, reward_model):
|
| 35 |
+
rewards = torch.zeros(len(responses), device=args.device)
|
| 36 |
+
|
| 37 |
+
with torch.no_grad():
|
| 38 |
+
reward_model_scores = []
|
| 39 |
+
batch_size = len(prompts)
|
| 40 |
+
|
| 41 |
+
for i in range(batch_size):
|
| 42 |
+
for j in range(args.num_generations):
|
| 43 |
+
response_idx = i * args.num_generations + j
|
| 44 |
+
response = responses[response_idx]
|
| 45 |
+
prompt = prompts[i]
|
| 46 |
+
|
| 47 |
+
pattern = r"<\|im_start\|>(system|user|assistant)\s+(.*?)<\|im_end\|>"
|
| 48 |
+
matches = re.findall(pattern, prompt, re.DOTALL)
|
| 49 |
+
messages = [{"role": role, "content": content.strip()} for role, content in matches]
|
| 50 |
+
answer = response
|
| 51 |
+
rewards[response_idx] += 0.5 if 20 <= len(response.strip()) <= 800 else -0.5
|
| 52 |
+
if '</think>' in response:
|
| 53 |
+
thinking_content, answer_content = response.split('</think>', 1)
|
| 54 |
+
rewards[response_idx] += 1.0 if 20 <= len(thinking_content.strip()) <= 300 else -0.5
|
| 55 |
+
rewards[response_idx] += 0.25 if response.count('</think>') == 1 else -0.25
|
| 56 |
+
answer = answer_content.strip()
|
| 57 |
+
rewards[response_idx] -= rep_penalty(answer)
|
| 58 |
+
|
| 59 |
+
score = reward_model.get_score(messages, answer)
|
| 60 |
+
reward_model_scores.append(score)
|
| 61 |
+
|
| 62 |
+
reward_model_scores = torch.tensor(reward_model_scores, device=args.device)
|
| 63 |
+
rewards += reward_model_scores
|
| 64 |
+
|
| 65 |
+
return rewards
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def grpo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, reward_model, start_step=0, wandb=None, use_sglang=False):
|
| 69 |
+
for step, batch in enumerate(loader, start=start_step + 1):
|
| 70 |
+
prompts = batch['prompt'] # list[str], length B
|
| 71 |
+
prompt_inputs = tokenizer(prompts, return_tensors="pt", padding=True, return_token_type_ids=False,
|
| 72 |
+
padding_side="left", add_special_tokens=False).to(args.device)
|
| 73 |
+
if args.max_seq_len:
|
| 74 |
+
prompt_inputs["input_ids"] = prompt_inputs["input_ids"][:, -args.max_seq_len:]
|
| 75 |
+
prompt_inputs["attention_mask"] = prompt_inputs["attention_mask"][:, -args.max_seq_len:]
|
| 76 |
+
|
| 77 |
+
rollout_result = rollout_engine.rollout(
|
| 78 |
+
prompt_ids=prompt_inputs["input_ids"],
|
| 79 |
+
attention_mask=prompt_inputs["attention_mask"],
|
| 80 |
+
num_generations=args.num_generations,
|
| 81 |
+
max_new_tokens=args.max_gen_len,
|
| 82 |
+
temperature=0.8,
|
| 83 |
+
)
|
| 84 |
+
outputs = rollout_result.output_ids
|
| 85 |
+
completion_ids = rollout_result.completion_ids
|
| 86 |
+
completions = rollout_result.completions
|
| 87 |
+
old_per_token_logps = rollout_result.per_token_logps.to(args.device).detach()
|
| 88 |
+
prompt_lens = rollout_result.prompt_lens.to(args.device)
|
| 89 |
+
full_mask = (outputs != tokenizer.pad_token_id).long()
|
| 90 |
+
logp_pos = prompt_lens.unsqueeze(1) - 1 + torch.arange(completion_ids.size(1), device=args.device).unsqueeze(0)
|
| 91 |
+
|
| 92 |
+
rewards = calculate_rewards(prompts, completions, reward_model).to(args.device) # [B*num_gen]
|
| 93 |
+
|
| 94 |
+
model_unwrapped = model.module if isinstance(model, DistributedDataParallel) else model
|
| 95 |
+
with autocast_ctx:
|
| 96 |
+
res = model_unwrapped(outputs, attention_mask=full_mask)
|
| 97 |
+
aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device)
|
| 98 |
+
per_token_logps = F.log_softmax(res.logits[:, :-1, :], dim=-1).gather(2, outputs[:, 1:].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos)
|
| 99 |
+
|
| 100 |
+
with torch.no_grad():
|
| 101 |
+
ref_per_token_logps = F.log_softmax(ref_model(outputs, attention_mask=full_mask).logits[:, :-1, :], dim=-1).gather(2, outputs[:, 1:].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos)
|
| 102 |
+
|
| 103 |
+
if args.debug_mode and is_main_process() and step % args.debug_interval == 0:
|
| 104 |
+
for i in range(len(prompts)):
|
| 105 |
+
Logger(f"[DEBUG] step={step}, sample[{i}]")
|
| 106 |
+
Logger('-'*100)
|
| 107 |
+
Logger(f"{'=' * 30} [DEBUG] sample[{i}] CONTEXT_BEGIN {'=' * 30}")
|
| 108 |
+
Logger(prompts[i])
|
| 109 |
+
Logger(f"{'=' * 31} [DEBUG] sample[{i}] CONTEXT_END {'=' * 31}")
|
| 110 |
+
for j in range(args.num_generations):
|
| 111 |
+
idx = i * args.num_generations + j
|
| 112 |
+
Logger(f"{'=' * 28} [DEBUG] gen[{j}] RESPONSE_BEGIN {'=' * 28}")
|
| 113 |
+
Logger(completions[idx])
|
| 114 |
+
Logger(f"{'=' * 29} [DEBUG] gen[{j}] RESPONSE_END {'=' * 29}")
|
| 115 |
+
Logger(f"[DEBUG] gen[{j}] reward={rewards[idx].item():.4f}")
|
| 116 |
+
Logger('='*100)
|
| 117 |
+
|
| 118 |
+
grouped_rewards = rewards.view(-1, args.num_generations) # [B, num_gen]
|
| 119 |
+
mean_r = grouped_rewards.mean(dim=1).repeat_interleave(args.num_generations) # [B*num_gen]
|
| 120 |
+
std_r = grouped_rewards.std(dim=1, unbiased=False).repeat_interleave(args.num_generations) # [B*num_gen]
|
| 121 |
+
advantages = (rewards - mean_r) / (std_r + 1e-4) # [B*num_gen]
|
| 122 |
+
|
| 123 |
+
completion_pad_mask = rollout_result.completion_mask.to(args.device).bool()
|
| 124 |
+
is_eos = (completion_ids == tokenizer.eos_token_id) & completion_pad_mask # [B*num_gen, R]
|
| 125 |
+
eos_idx = torch.full((is_eos.size(0),), is_eos.size(1) - 1, dtype=torch.long, device=args.device)
|
| 126 |
+
eos_idx[is_eos.any(dim=1)] = is_eos.int().argmax(dim=1)[is_eos.any(dim=1)]
|
| 127 |
+
completion_mask = ((torch.arange(is_eos.size(1), device=args.device).expand(is_eos.size(0), -1) <= eos_idx.unsqueeze(1)) & completion_pad_mask).int() # [B*num_gen, R]
|
| 128 |
+
|
| 129 |
+
kl_div = ref_per_token_logps - per_token_logps
|
| 130 |
+
per_token_kl = torch.exp(kl_div) - kl_div - 1 # [B*num_gen, R]
|
| 131 |
+
ratio = torch.exp(per_token_logps - old_per_token_logps) # [B*num_gen, R]
|
| 132 |
+
if args.loss_type == "cispo":
|
| 133 |
+
clamped_ratio = torch.clamp(ratio, max=args.epsilon_high).detach()
|
| 134 |
+
per_token_loss = -(clamped_ratio * advantages.unsqueeze(1) * per_token_logps - args.beta * per_token_kl)
|
| 135 |
+
else:
|
| 136 |
+
clipped_ratio = torch.clamp(ratio, 1 - args.epsilon, 1 + args.epsilon)
|
| 137 |
+
per_token_loss1 = ratio * advantages.unsqueeze(1)
|
| 138 |
+
per_token_loss2 = clipped_ratio * advantages.unsqueeze(1)
|
| 139 |
+
per_token_loss = -(torch.min(per_token_loss1, per_token_loss2) - args.beta * per_token_kl)
|
| 140 |
+
policy_loss = ((per_token_loss * completion_mask).sum(dim=1) / completion_mask.sum(dim=1).clamp(min=1)).mean()
|
| 141 |
+
loss = (policy_loss + aux_loss) / args.accumulation_steps # scalar
|
| 142 |
+
loss.backward()
|
| 143 |
+
|
| 144 |
+
if step % args.accumulation_steps == 0:
|
| 145 |
+
if args.grad_clip > 0:
|
| 146 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 147 |
+
optimizer.step()
|
| 148 |
+
scheduler.step()
|
| 149 |
+
optimizer.zero_grad()
|
| 150 |
+
|
| 151 |
+
if step % args.log_interval == 0 or step == iters:
|
| 152 |
+
policy_loss_val = loss.item() * args.accumulation_steps
|
| 153 |
+
current_aux_loss = aux_loss.item()
|
| 154 |
+
avg_reward_val = rewards.mean().item()
|
| 155 |
+
avg_len_val = completion_mask.sum(dim=1).float().mean().item()
|
| 156 |
+
kl_ref_val = ((ref_per_token_logps - per_token_logps) * completion_mask).sum().item() / max(completion_mask.sum().item(), 1)
|
| 157 |
+
advantages_mean_val = advantages.mean().item()
|
| 158 |
+
advantages_std_val = advantages.std().item()
|
| 159 |
+
current_lr = optimizer.param_groups[0]['lr']
|
| 160 |
+
|
| 161 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), '
|
| 162 |
+
f'Reward: {avg_reward_val:.4f}, KL_ref: {kl_ref_val:.4f}, '
|
| 163 |
+
f'Adv Std: {advantages_std_val:.4f}, Adv Mean: {advantages_mean_val:.4f}, '
|
| 164 |
+
f'Actor Loss: {policy_loss_val:.4f}, Avg Response Len: {avg_len_val:.2f}, Learning Rate: {current_lr:.8f}')
|
| 165 |
+
|
| 166 |
+
if wandb and is_main_process():
|
| 167 |
+
wandb.log({
|
| 168 |
+
"reward": avg_reward_val,
|
| 169 |
+
"kl_ref": kl_ref_val,
|
| 170 |
+
"advantages_std": advantages_std_val,
|
| 171 |
+
"advantages_mean": advantages_mean_val,
|
| 172 |
+
"policy_loss": policy_loss_val,
|
| 173 |
+
"avg_response_len": avg_len_val,
|
| 174 |
+
"learning_rate": current_lr
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 178 |
+
model.eval()
|
| 179 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 180 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 181 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 182 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 183 |
+
state_dict = raw_model.state_dict()
|
| 184 |
+
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
|
| 185 |
+
lm_checkpoint(lm_config, weight=args.save_weight, model=model, optimizer=optimizer,
|
| 186 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints', scheduler=scheduler)
|
| 187 |
+
model.train()
|
| 188 |
+
del state_dict
|
| 189 |
+
|
| 190 |
+
if step % args.save_interval == 0 or step == iters: rollout_engine.update_policy(model)
|
| 191 |
+
|
| 192 |
+
del prompt_inputs, outputs, completion_ids, per_token_logps, ref_per_token_logps
|
| 193 |
+
del completions, rewards, grouped_rewards, mean_r, std_r, advantages, completion_mask, completion_pad_mask, prompt_lens, logp_pos
|
| 194 |
+
|
| 195 |
+
if step > start_step and step % args.accumulation_steps != 0:
|
| 196 |
+
if args.grad_clip > 0:
|
| 197 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 198 |
+
optimizer.step()
|
| 199 |
+
scheduler.step()
|
| 200 |
+
optimizer.zero_grad()
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
if __name__ == "__main__":
|
| 204 |
+
parser = argparse.ArgumentParser(description="MiniMind GRPO (Group Relative Policy Optimization)")
|
| 205 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 206 |
+
parser.add_argument('--save_weight', default='grpo', type=str, help="保存权重的前缀名")
|
| 207 |
+
parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
|
| 208 |
+
parser.add_argument("--batch_size", type=int, default=2, help="batch size")
|
| 209 |
+
parser.add_argument("--learning_rate", type=float, default=3e-7, help="初始学习率")
|
| 210 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 211 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 212 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 213 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 214 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 215 |
+
parser.add_argument("--log_interval", type=int, default=1, help="日志打印间隔")
|
| 216 |
+
parser.add_argument("--save_interval", type=int, default=10, help="模型保存间隔")
|
| 217 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 218 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 219 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 220 |
+
parser.add_argument('--max_seq_len', default=768, type=int, help="Prompt最大长度")
|
| 221 |
+
parser.add_argument("--max_gen_len", type=int, default=1024, help="生成的最大长度")
|
| 222 |
+
parser.add_argument("--data_path", type=str, default="../dataset/rlaif.jsonl", help="RLAIF数据路径")
|
| 223 |
+
parser.add_argument("--num_generations", type=int, default=6, help="每个prompt生成的样本数")
|
| 224 |
+
parser.add_argument("--beta", type=float, default=0.1, help="KL惩罚系数")
|
| 225 |
+
parser.add_argument("--loss_type", type=str, default="cispo", choices=["grpo", "cispo"], help="loss类型")
|
| 226 |
+
parser.add_argument("--epsilon", type=float, default=0.2, help="GRPO的PPO clip epsilon")
|
| 227 |
+
parser.add_argument("--epsilon_high", type=float, default=5.0, help="epsilon上界")
|
| 228 |
+
parser.add_argument('--from_weight', default='full_sft', type=str, help="基于哪个权重训练")
|
| 229 |
+
parser.add_argument("--reward_model_path", type=str, default="../../internlm2-1_8b-reward", help="Reward模型路径")
|
| 230 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 231 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 232 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-GRPO", help="wandb项目名")
|
| 233 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 234 |
+
parser.add_argument("--debug_mode", action="store_true", help="是否打印训练调试采样")
|
| 235 |
+
parser.add_argument("--debug_interval", type=int, default=20, help="debug模式下每隔多少step打印一次采样")
|
| 236 |
+
parser.add_argument("--thinking_ratio", type=float, default=0.9, help="按概率开启thinking(0.0~1.0)")
|
| 237 |
+
parser.add_argument("--rollout_engine", type=str, default="torch", choices=["torch", "sglang"], help="rollout引擎类型")
|
| 238 |
+
parser.add_argument("--sglang_base_url", type=str, default="http://localhost:8998", help="SGLang服务器URL")
|
| 239 |
+
parser.add_argument("--sglang_model_path", type=str, default="../model", help="SGLang tokenizer路径")
|
| 240 |
+
parser.add_argument("--sglang_shared_path", type=str, default="./sglang_ckpt_grpo", help="SGLang共享存储路径")
|
| 241 |
+
args = parser.parse_args()
|
| 242 |
+
|
| 243 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 244 |
+
local_rank = init_distributed_mode()
|
| 245 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 246 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 247 |
+
|
| 248 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 249 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 250 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers,
|
| 251 |
+
max_seq_len=args.max_seq_len + args.max_gen_len, use_moe=bool(args.use_moe))
|
| 252 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 253 |
+
|
| 254 |
+
# ========== 3. 设置混合精度 ==========
|
| 255 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 256 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 257 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 258 |
+
|
| 259 |
+
# ========== 4. 配wandb ==========
|
| 260 |
+
wandb = None
|
| 261 |
+
if args.use_wandb and is_main_process():
|
| 262 |
+
import swanlab as wandb
|
| 263 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 264 |
+
resume = 'must' if wandb_id else None
|
| 265 |
+
wandb_run_name = f"MiniMind-GRPO-Epoch-{args.epochs}-BS-{args.batch_size}-LR-{args.learning_rate}"
|
| 266 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 267 |
+
|
| 268 |
+
# ========== 5. 初始化模型和数据 ==========
|
| 269 |
+
base_weight = args.from_weight
|
| 270 |
+
# Policy模型
|
| 271 |
+
model, tokenizer = init_model(lm_config, base_weight, device=args.device)
|
| 272 |
+
# Reference模型
|
| 273 |
+
ref_model, _ = init_model(lm_config, base_weight, device=args.device)
|
| 274 |
+
ref_model = ref_model.eval().requires_grad_(False)
|
| 275 |
+
# Reward模型
|
| 276 |
+
reward_model = LMForRewardModel(args.reward_model_path, device=args.device, dtype=torch.float16)
|
| 277 |
+
# Rollout引擎(可插拔替换,只负责 policy 推理)
|
| 278 |
+
rollout_engine = create_rollout_engine(
|
| 279 |
+
engine_type=args.rollout_engine,
|
| 280 |
+
policy_model=model,
|
| 281 |
+
tokenizer=tokenizer,
|
| 282 |
+
device=args.device,
|
| 283 |
+
autocast_ctx=autocast_ctx,
|
| 284 |
+
sglang_base_url=args.sglang_base_url,
|
| 285 |
+
sglang_model_path=args.sglang_model_path,
|
| 286 |
+
sglang_shared_path=args.sglang_shared_path,
|
| 287 |
+
)
|
| 288 |
+
# 数据和优化器
|
| 289 |
+
train_ds = RLAIFDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len, thinking_ratio=args.thinking_ratio)
|
| 290 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 291 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 292 |
+
loader_for_count = DataLoader(train_ds, batch_size=args.batch_size, sampler=train_sampler)
|
| 293 |
+
iters = len(loader_for_count)
|
| 294 |
+
total_optimizer_steps = math.ceil(iters / args.accumulation_steps) * args.epochs
|
| 295 |
+
scheduler = CosineAnnealingLR(optimizer, T_max=total_optimizer_steps, eta_min=args.learning_rate / 10)
|
| 296 |
+
|
| 297 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 298 |
+
start_epoch, start_step = 0, 0
|
| 299 |
+
if ckp_data:
|
| 300 |
+
model.load_state_dict(ckp_data['model'])
|
| 301 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 302 |
+
scheduler.load_state_dict(ckp_data['scheduler'])
|
| 303 |
+
start_epoch = ckp_data['epoch']
|
| 304 |
+
start_step = ckp_data.get('step', 0)
|
| 305 |
+
|
| 306 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 307 |
+
if args.use_compile == 1:
|
| 308 |
+
model = torch.compile(model)
|
| 309 |
+
Logger('torch.compile enabled')
|
| 310 |
+
rollout_engine.update_policy(model)
|
| 311 |
+
if dist.is_initialized():
|
| 312 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 313 |
+
rollout_engine.update_policy(model)
|
| 314 |
+
|
| 315 |
+
# ========== 8. 开始训练 ==========
|
| 316 |
+
for epoch in range(start_epoch, args.epochs):
|
| 317 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 318 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 319 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 320 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 321 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 322 |
+
if skip > 0:
|
| 323 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 324 |
+
grpo_train_epoch(epoch, loader, len(loader) + skip, rollout_engine, ref_model, reward_model, start_step, wandb, use_sglang = (args.rollout_engine == "sglang"))
|
| 325 |
+
else:
|
| 326 |
+
grpo_train_epoch(epoch, loader, len(loader), rollout_engine, ref_model, reward_model, 0, wandb, use_sglang = (args.rollout_engine == "sglang"))
|
| 327 |
+
|
| 328 |
+
# ========== 9. 清理分布进程 ==========
|
| 329 |
+
if dist.is_initialized():
|
| 330 |
+
dist.barrier()
|
| 331 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import time
|
| 7 |
+
import warnings
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from contextlib import nullcontext
|
| 11 |
+
from torch import optim, nn
|
| 12 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 13 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 14 |
+
from omni.models.minimind import MiniMindConfig
|
| 15 |
+
from omni.datasets.lm_dataset import SFTDataset
|
| 16 |
+
from omni.models.lora import save_lora, apply_lora
|
| 17 |
+
from omni.utils.training import get_lr, Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, init_model, SkipBatchSampler
|
| 18 |
+
|
| 19 |
+
warnings.filterwarnings('ignore')
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def train_epoch(epoch, loader, iters, lora_params, start_step=0, wandb=None):
|
| 23 |
+
start_time = time.time()
|
| 24 |
+
last_step = start_step
|
| 25 |
+
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
| 26 |
+
input_ids = input_ids.to(args.device)
|
| 27 |
+
labels = labels.to(args.device)
|
| 28 |
+
last_step = step
|
| 29 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 30 |
+
for param_group in optimizer.param_groups:
|
| 31 |
+
param_group['lr'] = lr
|
| 32 |
+
|
| 33 |
+
with autocast_ctx:
|
| 34 |
+
res = model(input_ids, labels=labels)
|
| 35 |
+
loss = res.loss + res.aux_loss
|
| 36 |
+
loss = loss / args.accumulation_steps
|
| 37 |
+
|
| 38 |
+
scaler.scale(loss).backward()
|
| 39 |
+
|
| 40 |
+
if step % args.accumulation_steps == 0:
|
| 41 |
+
scaler.unscale_(optimizer)
|
| 42 |
+
torch.nn.utils.clip_grad_norm_(lora_params, args.grad_clip)
|
| 43 |
+
scaler.step(optimizer)
|
| 44 |
+
scaler.update()
|
| 45 |
+
optimizer.zero_grad(set_to_none=True)
|
| 46 |
+
|
| 47 |
+
if step % args.log_interval == 0 or step == iters:
|
| 48 |
+
spend_time = time.time() - start_time
|
| 49 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 50 |
+
current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0
|
| 51 |
+
current_logits_loss = current_loss - current_aux_loss
|
| 52 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 53 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 54 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, logits_loss: {current_logits_loss:.4f}, aux_loss: {current_aux_loss:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')
|
| 55 |
+
if wandb: wandb.log({"loss": current_loss, "logits_loss": current_logits_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})
|
| 56 |
+
|
| 57 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 58 |
+
model.eval()
|
| 59 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 60 |
+
lora_save_path = f'{args.save_dir}/{args.lora_name}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 61 |
+
# LoRA只保存LoRA权重
|
| 62 |
+
save_lora(model, lora_save_path)
|
| 63 |
+
lm_checkpoint(lm_config, weight=args.lora_name, model=model, optimizer=optimizer, scaler=scaler, epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints')
|
| 64 |
+
model.train()
|
| 65 |
+
|
| 66 |
+
del input_ids, labels, res, loss
|
| 67 |
+
|
| 68 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 69 |
+
scaler.unscale_(optimizer)
|
| 70 |
+
torch.nn.utils.clip_grad_norm_(lora_params, args.grad_clip)
|
| 71 |
+
scaler.step(optimizer)
|
| 72 |
+
scaler.update()
|
| 73 |
+
optimizer.zero_grad(set_to_none=True)
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
parser = argparse.ArgumentParser(description="MiniMind LoRA Fine-tuning")
|
| 77 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 78 |
+
parser.add_argument("--lora_name", type=str, default="lora_medical", help="LoRA权重名称(如lora_identity/lora_medical等)")
|
| 79 |
+
parser.add_argument("--epochs", type=int, default=10, help="训练轮数")
|
| 80 |
+
parser.add_argument("--batch_size", type=int, default=32, help="batch size")
|
| 81 |
+
parser.add_argument("--learning_rate", type=float, default=1e-4, help="初始学习率")
|
| 82 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 83 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 84 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 85 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 86 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 87 |
+
parser.add_argument("--log_interval", type=int, default=10, help="日志打印间隔")
|
| 88 |
+
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
|
| 89 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 90 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 91 |
+
parser.add_argument('--max_seq_len', default=340, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
|
| 92 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 93 |
+
parser.add_argument("--data_path", type=str, default="../dataset/lora_medical.jsonl", help="LoRA训练数据路径")
|
| 94 |
+
parser.add_argument('--from_weight', default='full_sft', type=str, help="基于哪个权重训练,默认full_sft")
|
| 95 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 96 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 97 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-LoRA", help="wandb项目名")
|
| 98 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 99 |
+
args = parser.parse_args()
|
| 100 |
+
|
| 101 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 102 |
+
local_rank = init_distributed_mode()
|
| 103 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 104 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 105 |
+
|
| 106 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 107 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 108 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe))
|
| 109 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.lora_name, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 110 |
+
|
| 111 |
+
# ========== 3. 设置混合精度 ==========
|
| 112 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 113 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 114 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 115 |
+
|
| 116 |
+
# ========== 4. 配wandb ==========
|
| 117 |
+
wandb = None
|
| 118 |
+
if args.use_wandb and is_main_process():
|
| 119 |
+
import swanlab as wandb
|
| 120 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 121 |
+
resume = 'must' if wandb_id else None
|
| 122 |
+
wandb_run_name = f"MiniMind-LoRA-{args.lora_name}-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LR-{args.learning_rate}"
|
| 123 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 124 |
+
|
| 125 |
+
# ========== 5. 定义模型、应用LoRA、冻结非LoRA参数 ==========
|
| 126 |
+
model, tokenizer = init_model(lm_config, args.from_weight, device=args.device)
|
| 127 |
+
apply_lora(model)
|
| 128 |
+
|
| 129 |
+
# 统计参数
|
| 130 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 131 |
+
lora_params_count = sum(p.numel() for name, p in model.named_parameters() if 'lora' in name)
|
| 132 |
+
Logger(f"LLM 总参数量: {total_params / 1e6:.3f} M")
|
| 133 |
+
Logger(f"LoRA 参数量: {lora_params_count / 1e6:.3f} M")
|
| 134 |
+
Logger(f"LoRA 参数占比: {lora_params_count / total_params * 100:.2f}%")
|
| 135 |
+
|
| 136 |
+
# 冻结非LoRA参数,收集LoRA参数
|
| 137 |
+
lora_params = []
|
| 138 |
+
for name, param in model.named_parameters():
|
| 139 |
+
if 'lora' in name:
|
| 140 |
+
param.requires_grad = True
|
| 141 |
+
lora_params.append(param)
|
| 142 |
+
else:
|
| 143 |
+
param.requires_grad = False
|
| 144 |
+
|
| 145 |
+
# ========== 6. 定义数据和优化器 ==========
|
| 146 |
+
train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
| 147 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 148 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 149 |
+
optimizer = optim.AdamW(lora_params, lr=args.learning_rate)
|
| 150 |
+
|
| 151 |
+
# ========== 7. 从ckp恢复状态 ==========
|
| 152 |
+
start_epoch, start_step = 0, 0
|
| 153 |
+
if ckp_data:
|
| 154 |
+
model.load_state_dict(ckp_data['model'], strict=False)
|
| 155 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 156 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 157 |
+
start_epoch = ckp_data['epoch']
|
| 158 |
+
start_step = ckp_data.get('step', 0)
|
| 159 |
+
|
| 160 |
+
# ========== 8. 编译和分布式包装 ==========
|
| 161 |
+
if args.use_compile == 1:
|
| 162 |
+
args.use_compile = 0
|
| 163 |
+
Logger('[LoRA] monkey-patch forward 与 torch.compile 不兼容,use_compile 已自动关闭')
|
| 164 |
+
if dist.is_initialized():
|
| 165 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 166 |
+
|
| 167 |
+
# ========== 9. 开始训练 ==========
|
| 168 |
+
for epoch in range(start_epoch, args.epochs):
|
| 169 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 170 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 171 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 172 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 173 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 174 |
+
if skip > 0:
|
| 175 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 176 |
+
train_epoch(epoch, loader, len(loader) + skip, lora_params, start_step, wandb)
|
| 177 |
+
else:
|
| 178 |
+
train_epoch(epoch, loader, len(loader), lora_params, 0, wandb)
|
| 179 |
+
|
| 180 |
+
# ========== 10. 清理分布进程 ==========
|
| 181 |
+
if dist.is_initialized():
|
| 182 |
+
dist.barrier()
|
| 183 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import math
|
| 7 |
+
import re
|
| 8 |
+
import warnings
|
| 9 |
+
import torch
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from transformers import AutoTokenizer
|
| 13 |
+
from contextlib import nullcontext
|
| 14 |
+
from torch import optim, nn
|
| 15 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 16 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 17 |
+
from torch.nn.utils import clip_grad_norm_
|
| 18 |
+
from torch.optim.lr_scheduler import CosineAnnealingLR
|
| 19 |
+
from omni.models.minimind import MiniMindConfig, MiniMindForCausalLM
|
| 20 |
+
from omni.datasets.lm_dataset import RLAIFDataset
|
| 21 |
+
from omni.utils.training import Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, SkipBatchSampler, init_model, LMForRewardModel
|
| 22 |
+
from omni.trainers.rollout_engine import create_rollout_engine
|
| 23 |
+
|
| 24 |
+
warnings.filterwarnings('ignore')
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def rep_penalty(text, n=3, cap=0.5):
|
| 28 |
+
toks = re.findall(r"\w+|[^\w\s]", text.lower())
|
| 29 |
+
grams = [tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)]
|
| 30 |
+
return min(cap, (len(grams) - len(set(grams))) * cap * 2 / len(grams)) if grams else 0.0
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# 自定义的Critic模型,继承自MiniMindLM
|
| 34 |
+
class CriticModel(MiniMindForCausalLM):
|
| 35 |
+
def __init__(self, params):
|
| 36 |
+
super().__init__(params)
|
| 37 |
+
# 替换lm_head为输出单一价值的线性层
|
| 38 |
+
self.value_head = nn.Linear(params.hidden_size, 1)
|
| 39 |
+
|
| 40 |
+
def forward(self, input_ids=None, attention_mask=None, **kwargs):
|
| 41 |
+
# 使用基础模型获取隐藏状态
|
| 42 |
+
outputs = self.model(input_ids=input_ids, attention_mask=attention_mask, **kwargs)
|
| 43 |
+
hidden_states = self.model.norm(outputs[0])
|
| 44 |
+
# 使用value_head获取价值估计
|
| 45 |
+
values = self.value_head(hidden_states).squeeze(-1)
|
| 46 |
+
return values
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def calculate_rewards(prompts, responses, reward_model):
|
| 50 |
+
rewards = torch.zeros(len(responses), device=args.device)
|
| 51 |
+
|
| 52 |
+
with torch.no_grad():
|
| 53 |
+
reward_model_scores = []
|
| 54 |
+
for i, (prompt, response) in enumerate(zip(prompts, responses)):
|
| 55 |
+
pattern = r"<\|im_start\|>(system|user|assistant)\s+(.*?)<\|im_end\|>"
|
| 56 |
+
matches = re.findall(pattern, prompt, re.DOTALL)
|
| 57 |
+
messages = [{"role": role, "content": content.strip()} for role, content in matches]
|
| 58 |
+
answer = response
|
| 59 |
+
rewards[i] += 0.5 if 20 <= len(response.strip()) <= 800 else -0.5
|
| 60 |
+
if '</think>' in response:
|
| 61 |
+
thinking_content, answer_content = response.split('</think>', 1)
|
| 62 |
+
rewards[i] += 1.0 if 20 <= len(thinking_content.strip()) <= 300 else -0.5
|
| 63 |
+
rewards[i] += 0.25 if response.count('</think>') == 1 else -0.25
|
| 64 |
+
answer = answer_content.strip()
|
| 65 |
+
rewards[i] -= rep_penalty(answer)
|
| 66 |
+
|
| 67 |
+
score = reward_model.get_score(messages, answer)
|
| 68 |
+
reward_model_scores.append(score)
|
| 69 |
+
|
| 70 |
+
reward_model_scores = torch.tensor(reward_model_scores, device=args.device)
|
| 71 |
+
rewards += reward_model_scores
|
| 72 |
+
|
| 73 |
+
return rewards
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def ppo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, actor_scheduler, critic_scheduler, reward_model, start_step=0, wandb=None, use_sglang=False):
|
| 77 |
+
actor_model.train()
|
| 78 |
+
critic_model.train()
|
| 79 |
+
grad_accum_step = 0
|
| 80 |
+
|
| 81 |
+
for step, batch in enumerate(loader, start=start_step + 1):
|
| 82 |
+
prompts = batch["prompt"] # list[str], length B
|
| 83 |
+
enc = tokenizer(prompts, return_tensors="pt", padding=True, truncation=True, max_length=args.max_seq_len,
|
| 84 |
+
padding_side="left").to(args.device) # input_ids: [B, P], attention_mask: [B, P]
|
| 85 |
+
|
| 86 |
+
rollout_result = rollout_engine.rollout(
|
| 87 |
+
prompt_ids=enc.input_ids,
|
| 88 |
+
attention_mask=enc.attention_mask,
|
| 89 |
+
num_generations=1,
|
| 90 |
+
max_new_tokens=args.max_gen_len,
|
| 91 |
+
temperature=0.8,
|
| 92 |
+
)
|
| 93 |
+
gen_out = rollout_result.output_ids
|
| 94 |
+
completion_ids = rollout_result.completion_ids
|
| 95 |
+
prompt_lens = rollout_result.prompt_lens.to(args.device)
|
| 96 |
+
responses_text = rollout_result.completions
|
| 97 |
+
old_resp_logp = rollout_result.per_token_logps.to(args.device)
|
| 98 |
+
rewards = calculate_rewards(prompts, responses_text, reward_model) # [B]
|
| 99 |
+
|
| 100 |
+
if args.debug_mode and is_main_process() and step % args.debug_interval == 0:
|
| 101 |
+
for i in range(len(prompts)):
|
| 102 |
+
Logger(f"[DEBUG] step={step}, sample[{i}]")
|
| 103 |
+
Logger('-'*100)
|
| 104 |
+
Logger(f"{'=' * 30} [DEBUG] sample[{i}] CONTEXT_BEGIN {'=' * 30}")
|
| 105 |
+
Logger(prompts[i])
|
| 106 |
+
Logger(f"{'=' * 31} [DEBUG] sample[{i}] CONTEXT_END {'=' * 31}")
|
| 107 |
+
Logger(f"[DEBUG] prompt_len={prompt_lens[i].item()}, response_len={len(responses_text[i])}")
|
| 108 |
+
Logger(f"{'=' * 28} [DEBUG] sample[{i}] RESPONSE_BEGIN {'=' * 28}")
|
| 109 |
+
Logger(responses_text[i])
|
| 110 |
+
Logger(f"{'=' * 29} [DEBUG] sample[{i}] RESPONSE_END {'=' * 29}")
|
| 111 |
+
Logger(f"[DEBUG] reward={rewards[i].item():.4f}")
|
| 112 |
+
Logger('='*100)
|
| 113 |
+
|
| 114 |
+
full_mask = (gen_out != tokenizer.pad_token_id).long() # [B, P+R]
|
| 115 |
+
labels = gen_out[:, 1:].clone() # [B, P+R-1]
|
| 116 |
+
B = len(prompts)
|
| 117 |
+
resp_labels = completion_ids
|
| 118 |
+
resp_idx = torch.arange(resp_labels.size(1), device=gen_out.device).unsqueeze(0)
|
| 119 |
+
logp_pos = prompt_lens.unsqueeze(1) - 1 + resp_idx
|
| 120 |
+
resp_pad_mask = rollout_result.completion_mask.to(args.device).bool()
|
| 121 |
+
resp_lengths = resp_pad_mask.sum(dim=1); valid_resp = resp_lengths > 0; eos_mask = resp_labels.eq(tokenizer.eos_token_id) & resp_pad_mask
|
| 122 |
+
has_eos = eos_mask.any(dim=1); eos_pos = torch.argmax(eos_mask.int(), dim=1)
|
| 123 |
+
resp_lengths = torch.where(has_eos, eos_pos + 1, resp_lengths).long().clamp(min=1)
|
| 124 |
+
resp_policy_mask = ((resp_idx < resp_lengths.unsqueeze(1)) & resp_pad_mask).float()
|
| 125 |
+
resp_value_mask = resp_policy_mask.clone()
|
| 126 |
+
|
| 127 |
+
with torch.no_grad(): # Rollout阶段只需推理获取old_logp和old_values,切断梯度省显存
|
| 128 |
+
critic_for_rollout = critic_model.module if isinstance(critic_model, DistributedDataParallel) else critic_model
|
| 129 |
+
values_seq = critic_for_rollout(input_ids=gen_out, attention_mask=full_mask)
|
| 130 |
+
old_resp_values = values_seq.gather(1, logp_pos) * resp_value_mask
|
| 131 |
+
|
| 132 |
+
ref_resp_logp = F.log_softmax(ref_model(input_ids=gen_out, attention_mask=full_mask).logits[:, :-1], dim=-1).gather(2, labels.unsqueeze(-1)).squeeze(-1).gather(1, logp_pos)
|
| 133 |
+
token_rewards = torch.zeros_like(old_resp_logp)
|
| 134 |
+
last_idx = resp_lengths - 1 # [B]
|
| 135 |
+
token_rewards[torch.arange(B, device=args.device)[valid_resp], last_idx[valid_resp]] += rewards[valid_resp] # 末尾加外部奖励
|
| 136 |
+
|
| 137 |
+
gen_len = old_resp_values.size(1); lastgaelam = torch.zeros(B, device=args.device); advs_rev = []
|
| 138 |
+
for t in reversed(range(gen_len)):
|
| 139 |
+
nv = old_resp_values[:, t + 1] if t < gen_len - 1 else 0.0
|
| 140 |
+
delta = token_rewards[:, t] + args.gamma * nv - old_resp_values[:, t]
|
| 141 |
+
lastgaelam = delta + args.gamma * args.lam * lastgaelam
|
| 142 |
+
advs_rev.append(lastgaelam)
|
| 143 |
+
advantages = torch.stack(advs_rev[::-1], dim=1) # [B, R]
|
| 144 |
+
returns = advantages + old_resp_values # [B, R]
|
| 145 |
+
|
| 146 |
+
adv_mean = (advantages * resp_policy_mask).sum() / resp_policy_mask.sum().clamp(min=1)
|
| 147 |
+
adv_var = ((advantages - adv_mean) ** 2 * resp_policy_mask).sum() / resp_policy_mask.sum().clamp(min=1)
|
| 148 |
+
advantages = (advantages - adv_mean) * torch.rsqrt(adv_var + 1e-8) * resp_policy_mask
|
| 149 |
+
|
| 150 |
+
mb_size = max(1, min(args.mini_batch_size, B))
|
| 151 |
+
stop_ppo = False
|
| 152 |
+
policy_loss_sum = 0.0
|
| 153 |
+
value_loss_sum = 0.0
|
| 154 |
+
kl_sum = 0.0
|
| 155 |
+
kl_ref_sum = 0.0
|
| 156 |
+
clipfrac_sum = 0.0
|
| 157 |
+
aux_loss_sum = 0.0
|
| 158 |
+
log_count = 0
|
| 159 |
+
actor_unwrapped = actor_model.module if isinstance(actor_model, DistributedDataParallel) else actor_model
|
| 160 |
+
critic_unwrapped = critic_model.module if isinstance(critic_model, DistributedDataParallel) else critic_model
|
| 161 |
+
for ppo_epoch in range(args.ppo_update_iters):
|
| 162 |
+
if stop_ppo:
|
| 163 |
+
break
|
| 164 |
+
b_inds = torch.randperm(B, device=args.device)
|
| 165 |
+
for i in range(0, B, mb_size):
|
| 166 |
+
inds = b_inds[i:i + mb_size]
|
| 167 |
+
|
| 168 |
+
mb_values_seq = critic_unwrapped(input_ids=gen_out[inds], attention_mask=full_mask[inds])
|
| 169 |
+
mb_resp_values = mb_values_seq.gather(1, logp_pos[inds])
|
| 170 |
+
|
| 171 |
+
with autocast_ctx:
|
| 172 |
+
res = actor_unwrapped(input_ids=gen_out[inds], attention_mask=full_mask[inds])
|
| 173 |
+
aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device)
|
| 174 |
+
|
| 175 |
+
mb_resp_logp = F.log_softmax(res.logits[:, :-1], dim=-1).gather(2, labels[inds].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos[inds])
|
| 176 |
+
|
| 177 |
+
log_ratio = mb_resp_logp - old_resp_logp[inds]
|
| 178 |
+
approx_kl = (0.5 * (log_ratio ** 2) * resp_policy_mask[inds]).sum() / resp_policy_mask[inds].sum().clamp(min=1)
|
| 179 |
+
|
| 180 |
+
# 同步各卡的 approx_kl,防止某卡 break 而其它卡继续导致 DDP 死锁
|
| 181 |
+
approx_kl_val = approx_kl.detach().clone()
|
| 182 |
+
if dist.is_initialized():
|
| 183 |
+
dist.all_reduce(approx_kl_val, op=dist.ReduceOp.AVG)
|
| 184 |
+
|
| 185 |
+
if approx_kl_val > args.early_stop_kl:
|
| 186 |
+
stop_ppo = True
|
| 187 |
+
|
| 188 |
+
ratio = torch.exp(log_ratio)
|
| 189 |
+
clipfrac = ((((ratio - 1.0).abs() > args.clip_epsilon).float() * resp_policy_mask[inds]).sum()
|
| 190 |
+
/ resp_policy_mask[inds].sum().clamp(min=1))
|
| 191 |
+
kl_ref_penalty = ((torch.exp(ref_resp_logp[inds] - mb_resp_logp) - (ref_resp_logp[inds] - mb_resp_logp) - 1.0)
|
| 192 |
+
* resp_policy_mask[inds]).sum() / resp_policy_mask[inds].sum().clamp(min=1)
|
| 193 |
+
policy_loss = ((torch.max(-advantages[inds] * ratio,
|
| 194 |
+
-advantages[inds] * torch.clamp(ratio, 1.0 - args.clip_epsilon, 1.0 + args.clip_epsilon))
|
| 195 |
+
* resp_policy_mask[inds]).sum() / resp_policy_mask[inds].sum().clamp(min=1)
|
| 196 |
+
+ args.kl_coef * kl_ref_penalty)
|
| 197 |
+
value_loss = 0.5 * (torch.max((mb_resp_values - returns[inds]) ** 2,
|
| 198 |
+
(torch.clamp(mb_resp_values, old_resp_values[inds] - args.cliprange_value,
|
| 199 |
+
old_resp_values[inds] + args.cliprange_value) - returns[inds]) ** 2)
|
| 200 |
+
* resp_value_mask[inds]).sum() / resp_value_mask[inds].sum().clamp(min=1)
|
| 201 |
+
|
| 202 |
+
kl = approx_kl_val
|
| 203 |
+
kl_ref = kl_ref_penalty.detach()
|
| 204 |
+
|
| 205 |
+
# 早停时必须保证 forward-backward 闭环,故只截断 loss 不中断 DDP 通信
|
| 206 |
+
if stop_ppo:
|
| 207 |
+
loss = (policy_loss + args.vf_coef * value_loss + aux_loss) * 0.0
|
| 208 |
+
else:
|
| 209 |
+
loss = (policy_loss + args.vf_coef * value_loss + aux_loss) / args.accumulation_steps
|
| 210 |
+
|
| 211 |
+
loss.backward()
|
| 212 |
+
|
| 213 |
+
policy_loss_sum += policy_loss.item()
|
| 214 |
+
value_loss_sum += value_loss.item()
|
| 215 |
+
kl_sum += kl.item()
|
| 216 |
+
kl_ref_sum += kl_ref.item()
|
| 217 |
+
clipfrac_sum += clipfrac.item()
|
| 218 |
+
aux_loss_sum += aux_loss.item()
|
| 219 |
+
log_count += 1
|
| 220 |
+
|
| 221 |
+
grad_accum_step += 1
|
| 222 |
+
|
| 223 |
+
if grad_accum_step % args.accumulation_steps == 0:
|
| 224 |
+
clip_grad_norm_(actor_model.parameters(), args.grad_clip)
|
| 225 |
+
clip_grad_norm_(critic_model.parameters(), args.grad_clip)
|
| 226 |
+
actor_optimizer.step()
|
| 227 |
+
critic_optimizer.step()
|
| 228 |
+
actor_scheduler.step()
|
| 229 |
+
critic_scheduler.step()
|
| 230 |
+
actor_optimizer.zero_grad()
|
| 231 |
+
critic_optimizer.zero_grad()
|
| 232 |
+
|
| 233 |
+
if grad_accum_step % args.accumulation_steps != 0:
|
| 234 |
+
clip_grad_norm_(actor_model.parameters(), args.grad_clip)
|
| 235 |
+
clip_grad_norm_(critic_model.parameters(), args.grad_clip)
|
| 236 |
+
actor_optimizer.step()
|
| 237 |
+
critic_optimizer.step()
|
| 238 |
+
actor_scheduler.step()
|
| 239 |
+
critic_scheduler.step()
|
| 240 |
+
actor_optimizer.zero_grad()
|
| 241 |
+
critic_optimizer.zero_grad()
|
| 242 |
+
|
| 243 |
+
if step % args.save_interval == 0 or step == iters: rollout_engine.update_policy(actor_model)
|
| 244 |
+
|
| 245 |
+
if is_main_process():
|
| 246 |
+
critic_loss_val = value_loss_sum / max(log_count, 1)
|
| 247 |
+
reward_val = rewards.mean().item()
|
| 248 |
+
approx_kl_val = kl_sum / max(log_count, 1)
|
| 249 |
+
kl_ref_val = kl_ref_sum / max(log_count, 1)
|
| 250 |
+
clipfrac_val = clipfrac_sum / max(log_count, 1)
|
| 251 |
+
avg_len_val = resp_lengths.float().mean().item()
|
| 252 |
+
actor_lr, critic_lr = actor_optimizer.param_groups[0]['lr'], critic_optimizer.param_groups[0]['lr']
|
| 253 |
+
|
| 254 |
+
if wandb is not None:
|
| 255 |
+
wandb.log({
|
| 256 |
+
"reward": reward_val,
|
| 257 |
+
"kl_ref": kl_ref_val,
|
| 258 |
+
"approx_kl": approx_kl_val,
|
| 259 |
+
"clipfrac": clipfrac_val,
|
| 260 |
+
"critic_loss": critic_loss_val,
|
| 261 |
+
"avg_response_len": avg_len_val,
|
| 262 |
+
"actor_lr": actor_lr,
|
| 263 |
+
"critic_lr": critic_lr,
|
| 264 |
+
})
|
| 265 |
+
|
| 266 |
+
Logger(f"Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), "
|
| 267 |
+
f"Reward: {reward_val:.4f}, KL_ref: {kl_ref_val:.4f}, Approx KL: {approx_kl_val:.4f}, "
|
| 268 |
+
f"ClipFrac: {clipfrac_val:.4f}, Critic Loss: {critic_loss_val:.4f}, "
|
| 269 |
+
f"Avg Response Len: {avg_len_val:.2f}, Actor LR: {actor_lr:.8f}, Critic LR: {critic_lr:.8f}")
|
| 270 |
+
|
| 271 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 272 |
+
actor_model.eval()
|
| 273 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 274 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 275 |
+
raw_actor = actor_model.module if isinstance(actor_model, DistributedDataParallel) else actor_model
|
| 276 |
+
raw_actor = getattr(raw_actor, '_orig_mod', raw_actor)
|
| 277 |
+
actor_state = raw_actor.state_dict()
|
| 278 |
+
torch.save({k: v.half().cpu() for k, v in actor_state.items()}, ckp)
|
| 279 |
+
|
| 280 |
+
# 使用 lm_checkpoint 保存完整状态(包括 critic)
|
| 281 |
+
lm_checkpoint(lm_config, weight=args.save_weight, model=actor_model, optimizer=actor_optimizer,
|
| 282 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints',
|
| 283 |
+
scheduler=actor_scheduler, critic_model=critic_model,
|
| 284 |
+
critic_optimizer=critic_optimizer, critic_scheduler=critic_scheduler)
|
| 285 |
+
actor_model.train()
|
| 286 |
+
del actor_state
|
| 287 |
+
|
| 288 |
+
del enc, gen_out, completion_ids, responses_text, rewards, full_mask, values_seq, advantages
|
| 289 |
+
del labels, resp_labels, resp_idx, resp_pad_mask, valid_resp, eos_mask, has_eos, eos_pos, resp_lengths, resp_policy_mask, resp_value_mask, old_resp_logp, ref_resp_logp
|
| 290 |
+
del kl, kl_ref, policy_loss, value_loss, loss, token_rewards, returns, old_resp_values, prompt_lens, logp_pos
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
if __name__ == "__main__":
|
| 294 |
+
parser = argparse.ArgumentParser(description="MiniMind PPO (Proximal Policy Optimization)")
|
| 295 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 296 |
+
parser.add_argument('--save_weight', default='ppo_actor', type=str, help="保存权重的前缀名")
|
| 297 |
+
parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
|
| 298 |
+
parser.add_argument("--batch_size", type=int, default=2, help="batch size")
|
| 299 |
+
parser.add_argument("--learning_rate", type=float, default=3e-7, help="Actor学习率")
|
| 300 |
+
parser.add_argument("--critic_learning_rate", type=float, default=5e-7, help="Critic学习率")
|
| 301 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 302 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 303 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 304 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
|
| 305 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 306 |
+
parser.add_argument("--log_interval", type=int, default=1, help="日志打印间隔")
|
| 307 |
+
parser.add_argument("--save_interval", type=int, default=10, help="模型保存间隔")
|
| 308 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 309 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 310 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 311 |
+
parser.add_argument('--max_seq_len', default=768, type=int, help="Prompt最大长度")
|
| 312 |
+
parser.add_argument("--max_gen_len", type=int, default=1024, help="生成的最大长度")
|
| 313 |
+
parser.add_argument("--data_path", type=str, default="../dataset/rlaif.jsonl", help="RLAIF数据路径")
|
| 314 |
+
parser.add_argument("--clip_epsilon", type=float, default=0.2, help="PPO裁剪参数")
|
| 315 |
+
parser.add_argument("--vf_coef", type=float, default=0.5, help="Value function系数")
|
| 316 |
+
parser.add_argument("--kl_coef", type=float, default=0.02, help="KL散度惩罚系数")
|
| 317 |
+
parser.add_argument("--gamma", type=float, default=1.0, help="GAE折扣因子")
|
| 318 |
+
parser.add_argument("--lam", type=float, default=0.95, help="GAE lambda参数")
|
| 319 |
+
parser.add_argument("--cliprange_value", type=float, default=0.2, help="Value function裁剪范围")
|
| 320 |
+
parser.add_argument("--ppo_update_iters", type=int, default=2, help="同一批rollout重复更新次数")
|
| 321 |
+
parser.add_argument("--early_stop_kl", type=float, default=0.25, help="PPO early stop 的 KL 阈值")
|
| 322 |
+
parser.add_argument("--mini_batch_size", type=int, default=2, help="PPO每次更新的minibatch大小")
|
| 323 |
+
parser.add_argument('--from_weight', default='full_sft', type=str, help="基于哪个权重训练")
|
| 324 |
+
parser.add_argument("--reward_model_path", type=str, default="../../internlm2-1_8b-reward", help="Reward模型路径")
|
| 325 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 326 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 327 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-PPO", help="wandb项目名")
|
| 328 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 329 |
+
parser.add_argument("--debug_mode", action="store_true", help="是否打印训练调试采样")
|
| 330 |
+
parser.add_argument("--debug_interval", type=int, default=20, help="debug模式下每隔多少step打印一次采样")
|
| 331 |
+
parser.add_argument("--thinking_ratio", type=float, default=0.9, help="按概率开启thinking(0.0~1.0)")
|
| 332 |
+
parser.add_argument("--rollout_engine", type=str, default="torch", choices=["torch", "sglang"], help="rollout引擎类型")
|
| 333 |
+
parser.add_argument("--sglang_base_url", type=str, default="http://localhost:8998", help="SGLang服务器URL")
|
| 334 |
+
parser.add_argument("--sglang_model_path", type=str, default="../model", help="SGLang tokenizer路径")
|
| 335 |
+
parser.add_argument("--sglang_shared_path", type=str, default="./sglang_ckpt_ppo", help="SGLang共享存储路径")
|
| 336 |
+
args = parser.parse_args()
|
| 337 |
+
|
| 338 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 339 |
+
local_rank = init_distributed_mode()
|
| 340 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 341 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 342 |
+
|
| 343 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 344 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 345 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe))
|
| 346 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 347 |
+
|
| 348 |
+
# ========== 3. 设置混合精度 ==========
|
| 349 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 350 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 351 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 352 |
+
|
| 353 |
+
# ========== 4. 配wandb ==========
|
| 354 |
+
wandb = None
|
| 355 |
+
if args.use_wandb and is_main_process():
|
| 356 |
+
import swanlab as wandb
|
| 357 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 358 |
+
resume = 'must' if wandb_id else None
|
| 359 |
+
wandb_run_name = f"MiniMind-PPO-Epoch-{args.epochs}-BS-{args.batch_size}-LR-{args.learning_rate}"
|
| 360 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 361 |
+
|
| 362 |
+
# ========== 5. 初始化模型和数据 ==========
|
| 363 |
+
base_weight = args.from_weight
|
| 364 |
+
# Actor模型
|
| 365 |
+
actor_model, tokenizer = init_model(lm_config, base_weight, device=args.device)
|
| 366 |
+
ref_model, _ = init_model(lm_config, base_weight, device=args.device)
|
| 367 |
+
ref_model = ref_model.eval().requires_grad_(False)
|
| 368 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 369 |
+
ckp = f'{args.save_dir}/{base_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 370 |
+
state_dict = torch.load(ckp, map_location=args.device)
|
| 371 |
+
critic_model = CriticModel(lm_config)
|
| 372 |
+
critic_model.load_state_dict(state_dict, strict=False)
|
| 373 |
+
critic_model = critic_model.to(args.device)
|
| 374 |
+
reward_model = LMForRewardModel(args.reward_model_path, device=args.device, dtype=torch.float16)
|
| 375 |
+
# Rollout引擎
|
| 376 |
+
rollout_engine = create_rollout_engine(
|
| 377 |
+
engine_type=args.rollout_engine,
|
| 378 |
+
policy_model=actor_model,
|
| 379 |
+
tokenizer=tokenizer,
|
| 380 |
+
device=args.device,
|
| 381 |
+
autocast_ctx=autocast_ctx,
|
| 382 |
+
sglang_base_url=args.sglang_base_url,
|
| 383 |
+
sglang_model_path=args.sglang_model_path,
|
| 384 |
+
sglang_shared_path=args.sglang_shared_path,
|
| 385 |
+
)
|
| 386 |
+
train_ds = RLAIFDataset(args.data_path, tokenizer, max_length=(args.max_seq_len + args.max_gen_len), thinking_ratio=args.thinking_ratio)
|
| 387 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 388 |
+
actor_optimizer = optim.AdamW(actor_model.parameters(), lr=args.learning_rate)
|
| 389 |
+
critic_optimizer = optim.AdamW(critic_model.parameters(), lr=args.critic_learning_rate)
|
| 390 |
+
loader_for_count = DataLoader(train_ds, batch_size=args.batch_size, sampler=train_sampler)
|
| 391 |
+
iters = len(loader_for_count)
|
| 392 |
+
mb_factor = max(1, math.ceil(args.batch_size / args.mini_batch_size))
|
| 393 |
+
total_optimizer_steps = math.ceil(iters * args.epochs * args.ppo_update_iters * mb_factor / args.accumulation_steps)
|
| 394 |
+
actor_scheduler = CosineAnnealingLR(actor_optimizer, T_max=total_optimizer_steps, eta_min=args.learning_rate / 10)
|
| 395 |
+
critic_scheduler = CosineAnnealingLR(critic_optimizer, T_max=total_optimizer_steps, eta_min=args.critic_learning_rate / 10)
|
| 396 |
+
|
| 397 |
+
start_epoch, start_step = 0, 0
|
| 398 |
+
if ckp_data:
|
| 399 |
+
actor_model.load_state_dict(ckp_data['model'])
|
| 400 |
+
critic_model.load_state_dict(ckp_data['critic_model'])
|
| 401 |
+
actor_optimizer.load_state_dict(ckp_data['optimizer'])
|
| 402 |
+
critic_optimizer.load_state_dict(ckp_data['critic_optimizer'])
|
| 403 |
+
actor_scheduler.load_state_dict(ckp_data['scheduler'])
|
| 404 |
+
critic_scheduler.load_state_dict(ckp_data['critic_scheduler'])
|
| 405 |
+
start_epoch = ckp_data['epoch']
|
| 406 |
+
start_step = ckp_data.get('step', 0)
|
| 407 |
+
|
| 408 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 409 |
+
if args.use_compile == 1:
|
| 410 |
+
actor_model = torch.compile(actor_model)
|
| 411 |
+
Logger('torch.compile enabled')
|
| 412 |
+
rollout_engine.update_policy(actor_model)
|
| 413 |
+
if dist.is_initialized():
|
| 414 |
+
actor_model = DistributedDataParallel(actor_model, device_ids=[local_rank])
|
| 415 |
+
critic_model = DistributedDataParallel(critic_model, device_ids=[local_rank])
|
| 416 |
+
rollout_engine.update_policy(actor_model)
|
| 417 |
+
|
| 418 |
+
# ========== 8. 开始训练 ==========
|
| 419 |
+
for epoch in range(start_epoch, args.epochs):
|
| 420 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 421 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 422 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 423 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 424 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 425 |
+
if skip > 0:
|
| 426 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 427 |
+
ppo_train_epoch(epoch, loader, len(loader) + skip, rollout_engine, ref_model, actor_scheduler, critic_scheduler, reward_model, start_step, wandb, use_sglang = (args.rollout_engine == "sglang"))
|
| 428 |
+
else:
|
| 429 |
+
ppo_train_epoch(epoch, loader, len(loader), rollout_engine, ref_model, actor_scheduler, critic_scheduler, reward_model, 0, wandb, use_sglang = (args.rollout_engine == "sglang"))
|
| 430 |
+
|
| 431 |
+
# ========== 9. 清理分布进程 ==========
|
| 432 |
+
if dist.is_initialized():
|
| 433 |
+
dist.barrier()
|
| 434 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
|
| 5 |
+
import argparse
|
| 6 |
+
import time
|
| 7 |
+
import warnings
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from contextlib import nullcontext
|
| 11 |
+
from torch import optim, nn
|
| 12 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 13 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 14 |
+
from omni.models.minimind import MiniMindConfig
|
| 15 |
+
from omni.datasets.lm_dataset import PretrainDataset
|
| 16 |
+
from omni.utils.training import get_lr, Logger, is_main_process, lm_checkpoint, init_distributed_mode, setup_seed, init_model, SkipBatchSampler
|
| 17 |
+
|
| 18 |
+
warnings.filterwarnings('ignore')
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def train_epoch(epoch, loader, iters, start_step=0, wandb=None):
|
| 22 |
+
start_time = time.time()
|
| 23 |
+
last_step = start_step
|
| 24 |
+
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
| 25 |
+
input_ids = input_ids.to(args.device)
|
| 26 |
+
labels = labels.to(args.device)
|
| 27 |
+
last_step = step
|
| 28 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 29 |
+
for param_group in optimizer.param_groups:
|
| 30 |
+
param_group['lr'] = lr
|
| 31 |
+
|
| 32 |
+
with autocast_ctx:
|
| 33 |
+
res = model(input_ids, labels=labels)
|
| 34 |
+
loss = res.loss + res.aux_loss
|
| 35 |
+
loss = loss / args.accumulation_steps
|
| 36 |
+
|
| 37 |
+
scaler.scale(loss).backward()
|
| 38 |
+
|
| 39 |
+
if step % args.accumulation_steps == 0:
|
| 40 |
+
scaler.unscale_(optimizer)
|
| 41 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 42 |
+
|
| 43 |
+
scaler.step(optimizer)
|
| 44 |
+
scaler.update()
|
| 45 |
+
|
| 46 |
+
optimizer.zero_grad(set_to_none=True)
|
| 47 |
+
|
| 48 |
+
if step % args.log_interval == 0 or step == iters:
|
| 49 |
+
spend_time = time.time() - start_time
|
| 50 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 51 |
+
current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0
|
| 52 |
+
current_logits_loss = current_loss - current_aux_loss
|
| 53 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 54 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 55 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, logits_loss: {current_logits_loss:.4f}, aux_loss: {current_aux_loss:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')
|
| 56 |
+
if wandb: wandb.log({"loss": current_loss, "logits_loss": current_logits_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})
|
| 57 |
+
|
| 58 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 59 |
+
model.eval()
|
| 60 |
+
moe_suffix = '_moe' if lm_config.use_moe else ''
|
| 61 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
| 62 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 63 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 64 |
+
state_dict = raw_model.state_dict()
|
| 65 |
+
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
|
| 66 |
+
lm_checkpoint(lm_config, weight=args.save_weight, model=model, optimizer=optimizer, scaler=scaler, epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints')
|
| 67 |
+
model.train()
|
| 68 |
+
del state_dict
|
| 69 |
+
|
| 70 |
+
del input_ids, labels, res, loss
|
| 71 |
+
|
| 72 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 73 |
+
scaler.unscale_(optimizer)
|
| 74 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 75 |
+
scaler.step(optimizer)
|
| 76 |
+
scaler.update()
|
| 77 |
+
optimizer.zero_grad(set_to_none=True)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
parser = argparse.ArgumentParser(description="MiniMind Pretraining")
|
| 82 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 83 |
+
parser.add_argument('--save_weight', default='pretrain', type=str, help="保存权重的前缀名")
|
| 84 |
+
parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
|
| 85 |
+
parser.add_argument("--batch_size", type=int, default=32, help="batch size")
|
| 86 |
+
parser.add_argument("--learning_rate", type=float, default=5e-4, help="初始学习率")
|
| 87 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 88 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 89 |
+
parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")
|
| 90 |
+
parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")
|
| 91 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 92 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 93 |
+
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
|
| 94 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 95 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐���层数量")
|
| 96 |
+
parser.add_argument('--max_seq_len', default=340, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
|
| 97 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 98 |
+
parser.add_argument("--data_path", type=str, default="../dataset/pretrain_t2t_mini.jsonl", help="预训练数据路径")
|
| 99 |
+
parser.add_argument('--from_weight', default='none', type=str, help="基于哪个权重训练,为none则从头开始")
|
| 100 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 101 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 102 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-Pretrain", help="wandb项目名")
|
| 103 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 104 |
+
args = parser.parse_args()
|
| 105 |
+
|
| 106 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 107 |
+
local_rank = init_distributed_mode()
|
| 108 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 109 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 110 |
+
|
| 111 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 112 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 113 |
+
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe))
|
| 114 |
+
ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 115 |
+
|
| 116 |
+
# ========== 3. 设置混合精度 ==========
|
| 117 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 118 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 119 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 120 |
+
|
| 121 |
+
# ========== 4. 配wandb ==========
|
| 122 |
+
wandb = None
|
| 123 |
+
if args.use_wandb and is_main_process():
|
| 124 |
+
import swanlab as wandb
|
| 125 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 126 |
+
resume = 'must' if wandb_id else None
|
| 127 |
+
wandb_run_name = f"MiniMind-Pretrain-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"
|
| 128 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 129 |
+
|
| 130 |
+
# ========== 5. 定义模型、数据、优化器 ==========
|
| 131 |
+
model, tokenizer = init_model(lm_config, args.from_weight, device=args.device)
|
| 132 |
+
train_ds = PretrainDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
| 133 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 134 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 135 |
+
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
| 136 |
+
|
| 137 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 138 |
+
start_epoch, start_step = 0, 0
|
| 139 |
+
if ckp_data:
|
| 140 |
+
model.load_state_dict(ckp_data['model'])
|
| 141 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 142 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 143 |
+
start_epoch = ckp_data['epoch']
|
| 144 |
+
start_step = ckp_data.get('step', 0)
|
| 145 |
+
|
| 146 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 147 |
+
if args.use_compile == 1:
|
| 148 |
+
model = torch.compile(model)
|
| 149 |
+
Logger('torch.compile enabled')
|
| 150 |
+
if dist.is_initialized():
|
| 151 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 152 |
+
|
| 153 |
+
# ========== 8. 开始训练 ==========
|
| 154 |
+
for epoch in range(start_epoch, args.epochs):
|
| 155 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 156 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 157 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 158 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 159 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
| 160 |
+
if skip > 0:
|
| 161 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 162 |
+
train_epoch(epoch, loader, len(loader) + skip, start_step, wandb)
|
| 163 |
+
else:
|
| 164 |
+
train_epoch(epoch, loader, len(loader), 0, wandb)
|
| 165 |
+
|
| 166 |
+
# ========== 9. 清理分布进程 ==========
|
| 167 |
+
if dist.is_initialized():
|
| 168 |
+
dist.barrier()
|
| 169 |
+
dist.destroy_process_group()
|
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
import warnings
|
| 5 |
+
import datasets
|
| 6 |
+
import torch
|
| 7 |
+
import torch.distributed as dist
|
| 8 |
+
from contextlib import nullcontext
|
| 9 |
+
from torch import optim, nn
|
| 10 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 11 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 12 |
+
from transformers import AutoTokenizer
|
| 13 |
+
from omni.models import MiniMindVLM, VLMConfig
|
| 14 |
+
from omni.datasets.lm_dataset import VLMDataset
|
| 15 |
+
from omni.utils import get_lr, Logger, is_main_process, init_distributed_mode, setup_seed, init_vlm_model, vlm_checkpoint, SkipBatchSampler, vlm_collate_fn
|
| 16 |
+
|
| 17 |
+
warnings.filterwarnings('ignore')
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def train_epoch(epoch, loader, iters, start_step=0, wandb=None):
|
| 21 |
+
start_time = time.time()
|
| 22 |
+
last_step = start_step
|
| 23 |
+
for step, (input_ids, labels, pixel_values) in enumerate(loader, start=start_step + 1):
|
| 24 |
+
input_ids = input_ids.to(args.device)
|
| 25 |
+
labels = labels.to(args.device)
|
| 26 |
+
pixel_values = {k: v.to(args.device) for k, v in pixel_values.items()} if isinstance(pixel_values, dict) else pixel_values.to(args.device)
|
| 27 |
+
last_step = step
|
| 28 |
+
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
| 29 |
+
for param_group in optimizer.param_groups:
|
| 30 |
+
param_group['lr'] = lr
|
| 31 |
+
|
| 32 |
+
with autocast_ctx:
|
| 33 |
+
res = model(input_ids, labels=labels, pixel_values=pixel_values)
|
| 34 |
+
loss = res.loss + res.aux_loss
|
| 35 |
+
loss = loss / args.accumulation_steps
|
| 36 |
+
|
| 37 |
+
scaler.scale(loss).backward()
|
| 38 |
+
|
| 39 |
+
if step % args.accumulation_steps == 0:
|
| 40 |
+
scaler.unscale_(optimizer)
|
| 41 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 42 |
+
|
| 43 |
+
scaler.step(optimizer)
|
| 44 |
+
scaler.update()
|
| 45 |
+
|
| 46 |
+
optimizer.zero_grad(set_to_none=True)
|
| 47 |
+
|
| 48 |
+
if step % args.log_interval == 0 or step == iters:
|
| 49 |
+
spend_time = time.time() - start_time
|
| 50 |
+
current_loss = loss.item() * args.accumulation_steps
|
| 51 |
+
current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0
|
| 52 |
+
current_logits_loss = current_loss - current_aux_loss
|
| 53 |
+
current_lr = optimizer.param_groups[-1]['lr']
|
| 54 |
+
eta_min = spend_time / max(step - start_step, 1) * (iters - step) // 60
|
| 55 |
+
Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, logits_loss: {current_logits_loss:.4f}, aux_loss: {current_aux_loss:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')
|
| 56 |
+
if wandb: wandb.log({"loss": current_loss, "logits_loss": current_logits_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})
|
| 57 |
+
|
| 58 |
+
if (step % args.save_interval == 0 or step == iters) and is_main_process():
|
| 59 |
+
model.eval()
|
| 60 |
+
moe_suffix = '_moe' if vlm_config.use_moe else ''
|
| 61 |
+
ckp = f'{args.save_dir}/{args.save_weight}_{vlm_config.hidden_size}{moe_suffix}.pth'
|
| 62 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 63 |
+
raw_model = getattr(raw_model, '_orig_mod', raw_model)
|
| 64 |
+
state_dict = raw_model.state_dict()
|
| 65 |
+
clean_state_dict = {
|
| 66 |
+
key: value for key, value in state_dict.items() if not key.startswith('vision_encoder.')
|
| 67 |
+
}
|
| 68 |
+
clean_state_dict = {k: v.half().cpu() for k, v in clean_state_dict.items()} # 半精度保存并移到CPU
|
| 69 |
+
torch.save(clean_state_dict, ckp)
|
| 70 |
+
vlm_checkpoint(vlm_config, weight=args.save_weight, model=model, optimizer=optimizer,
|
| 71 |
+
epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints', scaler=scaler)
|
| 72 |
+
model.train()
|
| 73 |
+
del state_dict, clean_state_dict
|
| 74 |
+
|
| 75 |
+
del input_ids, labels, pixel_values, res, loss
|
| 76 |
+
|
| 77 |
+
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
| 78 |
+
scaler.unscale_(optimizer)
|
| 79 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 80 |
+
scaler.step(optimizer)
|
| 81 |
+
scaler.update()
|
| 82 |
+
optimizer.zero_grad(set_to_none=True)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
parser = argparse.ArgumentParser(description="MiniMind-V Pretrain")
|
| 87 |
+
parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")
|
| 88 |
+
parser.add_argument('--save_weight', default='pretrain_vlm', type=str, help="保存权重的前缀名")
|
| 89 |
+
parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
|
| 90 |
+
parser.add_argument("--batch_size", type=int, default=16, help="batch size")
|
| 91 |
+
parser.add_argument("--learning_rate", type=float, default=4e-4, help="初始学习率")
|
| 92 |
+
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
|
| 93 |
+
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
| 94 |
+
parser.add_argument("--num_workers", type=int, default=2, help="数据加载线程数")
|
| 95 |
+
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度��积步数")
|
| 96 |
+
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
| 97 |
+
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
|
| 98 |
+
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
|
| 99 |
+
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
|
| 100 |
+
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
|
| 101 |
+
parser.add_argument('--max_seq_len', default=450, type=int, help="训练的最大截断长度")
|
| 102 |
+
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
|
| 103 |
+
parser.add_argument("--data_path", type=str, default="../dataset/pretrain_i2t.parquet", help="训练数据路径")
|
| 104 |
+
parser.add_argument('--from_weight', default='llm', type=str, help="基于哪个权重训练,为none则不基于任何权重训练")
|
| 105 |
+
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
|
| 106 |
+
parser.add_argument('--freeze_llm', default=2, type=int, choices=[0, 1, 2], help="冻结策略(0=完全可训练,1=冻结+解冻首尾层,2=完全冻结仅训练proj)")
|
| 107 |
+
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
| 108 |
+
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")
|
| 109 |
+
parser.add_argument("--wandb_project", type=str, default="MiniMind-V-Pretrain", help="wandb项目名")
|
| 110 |
+
args = parser.parse_args()
|
| 111 |
+
|
| 112 |
+
# ========== 1. 初始化环境和随机种子 ==========
|
| 113 |
+
local_rank = init_distributed_mode()
|
| 114 |
+
if dist.is_initialized(): args.device = f"cuda:{local_rank}"
|
| 115 |
+
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
| 116 |
+
|
| 117 |
+
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
| 118 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 119 |
+
vlm_config = VLMConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, max_seq_len=args.max_seq_len, use_moe=bool(args.use_moe))
|
| 120 |
+
ckp_data = vlm_checkpoint(vlm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None
|
| 121 |
+
|
| 122 |
+
# ========== 3. 设置混合精度 ==========
|
| 123 |
+
device_type = "cuda" if "cuda" in args.device else "cpu"
|
| 124 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
| 125 |
+
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
| 126 |
+
|
| 127 |
+
# ========== 4. 配wandb ==========
|
| 128 |
+
wandb = None
|
| 129 |
+
if args.use_wandb and is_main_process():
|
| 130 |
+
import swanlab as wandb
|
| 131 |
+
wandb_id = ckp_data.get('wandb_id') if ckp_data else None
|
| 132 |
+
resume = 'must' if wandb_id else None
|
| 133 |
+
wandb_run_name = f"MiniMind-V-Pretrain-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"
|
| 134 |
+
wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)
|
| 135 |
+
|
| 136 |
+
# ========== 5. 定义模型、数据、优化器 ==========
|
| 137 |
+
model, tokenizer = init_vlm_model(vlm_config, from_weight=args.from_weight, device=args.device, freeze_llm=args.freeze_llm)
|
| 138 |
+
preprocess = model.vision_encoder.processor
|
| 139 |
+
train_ds = VLMDataset(args.data_path, tokenizer, preprocess=preprocess, image_special_token=vlm_config.image_special_token, image_token_len=vlm_config.image_token_len, max_length=vlm_config.max_seq_len)
|
| 140 |
+
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
| 141 |
+
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
| 142 |
+
optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=args.learning_rate)
|
| 143 |
+
|
| 144 |
+
# ========== 6. 从ckp恢复状态 ==========
|
| 145 |
+
start_epoch, start_step = 0, 0
|
| 146 |
+
if ckp_data:
|
| 147 |
+
model.load_state_dict(ckp_data['model'], strict=False)
|
| 148 |
+
optimizer.load_state_dict(ckp_data['optimizer'])
|
| 149 |
+
scaler.load_state_dict(ckp_data['scaler'])
|
| 150 |
+
start_epoch = ckp_data['epoch']
|
| 151 |
+
start_step = ckp_data.get('step', 0)
|
| 152 |
+
|
| 153 |
+
# ========== 7. 编译和分布式包装 ==========
|
| 154 |
+
if args.use_compile == 1:
|
| 155 |
+
model = torch.compile(model)
|
| 156 |
+
Logger('torch.compile enabled')
|
| 157 |
+
if dist.is_initialized():
|
| 158 |
+
model._ddp_params_and_buffers_to_ignore = {"freqs_cos", "freqs_sin"}
|
| 159 |
+
model = DistributedDataParallel(model, device_ids=[local_rank])
|
| 160 |
+
|
| 161 |
+
# ========== 8. 开始训练 ==========
|
| 162 |
+
for epoch in range(start_epoch, args.epochs):
|
| 163 |
+
train_sampler and train_sampler.set_epoch(epoch)
|
| 164 |
+
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()
|
| 165 |
+
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
|
| 166 |
+
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
|
| 167 |
+
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True, collate_fn=vlm_collate_fn)
|
| 168 |
+
if skip > 0:
|
| 169 |
+
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
| 170 |
+
train_epoch(epoch, loader, len(loader) + skip, start_step, wandb)
|
| 171 |
+
else:
|
| 172 |
+
train_epoch(epoch, loader, len(loader), 0, wandb)
|
| 173 |
+
|
| 174 |
+
# ========== 9. 清理分布进程 ==========
|
| 175 |
+
if dist.is_initialized(): dist.destroy_process_group()
|
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rollout engines for RL training.
|
| 2 |
+
|
| 3 |
+
To use the SGLang backend, first launch the server (with a transformers-format model):
|
| 4 |
+
python -m sglang.launch_server --model-path ./minimind-3 --attention-backend triton --host 0.0.0.0 --port 8998
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import requests
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from abc import ABC, abstractmethod
|
| 11 |
+
from contextlib import nullcontext
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from typing import List, Optional, Tuple
|
| 14 |
+
from torch import Tensor
|
| 15 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 16 |
+
from transformers import AutoTokenizer
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def compute_per_token_logps(model, input_ids: Tensor, n_keep: int, attention_mask: Optional[Tensor] = None) -> Tensor:
|
| 20 |
+
if n_keep <= 0:
|
| 21 |
+
return input_ids.new_empty((input_ids.size(0), 0), dtype=torch.float32)
|
| 22 |
+
unwrapped = model.module if isinstance(model, DistributedDataParallel) else model
|
| 23 |
+
input_ids = input_ids.detach().clone() if input_ids.is_inference() else input_ids
|
| 24 |
+
logits = unwrapped(input_ids, attention_mask=attention_mask, logits_to_keep=n_keep + 1).logits[:, :-1, :]
|
| 25 |
+
per_token_logps = []
|
| 26 |
+
for logits_row, ids_row in zip(logits, input_ids[:, -n_keep:]):
|
| 27 |
+
ids_row = ids_row.detach().clone() if ids_row.is_inference() else ids_row
|
| 28 |
+
per_token_logps.append(
|
| 29 |
+
torch.gather(logits_row.log_softmax(dim=-1), 1, ids_row.unsqueeze(1)).squeeze(1)
|
| 30 |
+
)
|
| 31 |
+
return torch.stack(per_token_logps)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class RolloutResult:
|
| 36 |
+
output_ids: Tensor
|
| 37 |
+
completion_ids: Tensor
|
| 38 |
+
per_token_logps: Tensor
|
| 39 |
+
completions: List[str]
|
| 40 |
+
prompt_lens: Tensor
|
| 41 |
+
completion_mask: Tensor
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class RolloutEngine(ABC):
|
| 45 |
+
tokenizer = None
|
| 46 |
+
|
| 47 |
+
@abstractmethod
|
| 48 |
+
def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int, max_new_tokens: int, temperature: float = 0.8) -> RolloutResult:
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
@abstractmethod
|
| 52 |
+
def update_policy(self, model: torch.nn.Module):
|
| 53 |
+
pass
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class TorchRolloutEngine(RolloutEngine):
|
| 57 |
+
def __init__(self, policy_model: torch.nn.Module, tokenizer, device: str = "cuda", autocast_ctx=None):
|
| 58 |
+
self.policy_model = policy_model
|
| 59 |
+
self.tokenizer = tokenizer
|
| 60 |
+
self.device = device
|
| 61 |
+
self.autocast_ctx = autocast_ctx
|
| 62 |
+
|
| 63 |
+
def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int, max_new_tokens: int, temperature: float = 0.8) -> RolloutResult:
|
| 64 |
+
model = self.policy_model.module if isinstance(self.policy_model, DistributedDataParallel) else self.policy_model
|
| 65 |
+
ctx = self.autocast_ctx if self.autocast_ctx else nullcontext()
|
| 66 |
+
with torch.no_grad(), ctx:
|
| 67 |
+
output_ids = model.generate(
|
| 68 |
+
input_ids=prompt_ids.repeat_interleave(num_generations, dim=0),
|
| 69 |
+
attention_mask=attention_mask.repeat_interleave(num_generations, dim=0),
|
| 70 |
+
max_new_tokens=max_new_tokens,
|
| 71 |
+
do_sample=True,
|
| 72 |
+
temperature=temperature,
|
| 73 |
+
num_return_sequences=1,
|
| 74 |
+
pad_token_id=self.tokenizer.pad_token_id,
|
| 75 |
+
eos_token_id=self.tokenizer.eos_token_id,
|
| 76 |
+
).clone()
|
| 77 |
+
prompt_len = prompt_ids.size(1)
|
| 78 |
+
completion_ids = output_ids[:, prompt_len:]
|
| 79 |
+
full_mask = (output_ids != self.tokenizer.pad_token_id).long()
|
| 80 |
+
per_token_logps = compute_per_token_logps(self.policy_model, output_ids, completion_ids.size(1), attention_mask=full_mask)
|
| 81 |
+
completions = self.tokenizer.batch_decode(completion_ids, skip_special_tokens=True)
|
| 82 |
+
return RolloutResult(output_ids, completion_ids, per_token_logps, completions,
|
| 83 |
+
prompt_ids.new_full((output_ids.size(0),), prompt_len),
|
| 84 |
+
attention_mask.new_ones(output_ids.size(0), completion_ids.size(1)))
|
| 85 |
+
|
| 86 |
+
def update_policy(self, model: torch.nn.Module):
|
| 87 |
+
self.policy_model = model
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class SGLangRolloutEngine(RolloutEngine):
|
| 91 |
+
def __init__(self, base_url: str, model_path: str, shared_ckpt_path: str = "./sglang_ckpt", timeout: int = 120):
|
| 92 |
+
self.base_url = base_url.rstrip('/')
|
| 93 |
+
self.shared_ckpt_path = shared_ckpt_path
|
| 94 |
+
self.timeout = timeout
|
| 95 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 96 |
+
self.http = requests
|
| 97 |
+
|
| 98 |
+
def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int, max_new_tokens: int, temperature: float = 0.8) -> RolloutResult:
|
| 99 |
+
input_ids_list = []
|
| 100 |
+
for ids, mask in zip(prompt_ids, attention_mask):
|
| 101 |
+
valid_ids = ids[mask.bool()].tolist()
|
| 102 |
+
input_ids_list.append(valid_ids)
|
| 103 |
+
all_input_ids = [ids for ids in input_ids_list for _ in range(num_generations)]
|
| 104 |
+
|
| 105 |
+
payload = {
|
| 106 |
+
"input_ids": all_input_ids,
|
| 107 |
+
"sampling_params": {
|
| 108 |
+
"temperature": temperature,
|
| 109 |
+
"max_new_tokens": max_new_tokens,
|
| 110 |
+
"stop_token_ids": [self.tokenizer.eos_token_id] if self.tokenizer.eos_token_id else [],
|
| 111 |
+
},
|
| 112 |
+
"return_logprob": True,
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
resp = self.http.post(f"{self.base_url}/generate", json=payload, timeout=self.timeout)
|
| 116 |
+
resp.raise_for_status()
|
| 117 |
+
|
| 118 |
+
results = resp.json()
|
| 119 |
+
if not isinstance(results, list):
|
| 120 |
+
results = [results]
|
| 121 |
+
|
| 122 |
+
all_output_ids, all_completion_ids, all_logprobs = [], [], []
|
| 123 |
+
completions = []
|
| 124 |
+
|
| 125 |
+
for i, result in enumerate(results):
|
| 126 |
+
meta = result.get("meta_info", {})
|
| 127 |
+
completion_ids = meta.get("output_ids", result.get("output_ids", []))
|
| 128 |
+
raw_logprobs = meta.get("output_token_logprobs", [])
|
| 129 |
+
|
| 130 |
+
logprobs = []
|
| 131 |
+
for item in raw_logprobs:
|
| 132 |
+
if isinstance(item, (list, tuple)) and len(item) >= 1:
|
| 133 |
+
logprobs.append(item[0])
|
| 134 |
+
elif isinstance(item, (int, float)):
|
| 135 |
+
logprobs.append(item)
|
| 136 |
+
|
| 137 |
+
if len(logprobs) < len(completion_ids):
|
| 138 |
+
logprobs = [0.0] * (len(completion_ids) - len(logprobs)) + logprobs
|
| 139 |
+
elif len(logprobs) > len(completion_ids):
|
| 140 |
+
logprobs = logprobs[-len(completion_ids):] if completion_ids else []
|
| 141 |
+
prompt = all_input_ids[i]
|
| 142 |
+
full_output = prompt + completion_ids
|
| 143 |
+
all_output_ids.append(full_output)
|
| 144 |
+
all_completion_ids.append(completion_ids)
|
| 145 |
+
all_logprobs.append(logprobs)
|
| 146 |
+
completions.append(self.tokenizer.decode(completion_ids, skip_special_tokens=True))
|
| 147 |
+
|
| 148 |
+
device = prompt_ids.device
|
| 149 |
+
max_comp_len = max(1, max(len(ids) for ids in all_completion_ids))
|
| 150 |
+
max_out_len = max(len(ids) for ids in all_input_ids) + max_comp_len
|
| 151 |
+
|
| 152 |
+
def pad_to_tensor(seqs, max_len, pad_val=0):
|
| 153 |
+
return torch.tensor([s + [pad_val] * (max_len - len(s)) for s in seqs], device=device)
|
| 154 |
+
|
| 155 |
+
pad_id = self.tokenizer.pad_token_id
|
| 156 |
+
return RolloutResult(
|
| 157 |
+
output_ids=pad_to_tensor(all_output_ids, max_out_len, pad_val=pad_id),
|
| 158 |
+
completion_ids=pad_to_tensor(all_completion_ids, max_comp_len, pad_val=pad_id),
|
| 159 |
+
per_token_logps=pad_to_tensor(all_logprobs, max_comp_len, pad_val=0.0),
|
| 160 |
+
completions=completions,
|
| 161 |
+
prompt_lens=torch.tensor([len(ids) for ids in all_input_ids], device=device),
|
| 162 |
+
completion_mask=torch.tensor([[1] * len(ids) + [0] * (max_comp_len - len(ids)) for ids in all_completion_ids], device=device),
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
def update_policy(self, model: torch.nn.Module):
|
| 166 |
+
ok = True
|
| 167 |
+
if not dist.is_initialized() or dist.get_rank() == 0:
|
| 168 |
+
try:
|
| 169 |
+
unwrapped = model.module if isinstance(model, DistributedDataParallel) else model
|
| 170 |
+
unwrapped = getattr(unwrapped, '_orig_mod', unwrapped)
|
| 171 |
+
abs_path = os.path.abspath(self.shared_ckpt_path)
|
| 172 |
+
state_dict = {k: v.detach().half().cpu() for k, v in unwrapped.state_dict().items()}
|
| 173 |
+
unwrapped.save_pretrained(abs_path, state_dict=state_dict, safe_serialization=False)
|
| 174 |
+
self.tokenizer.save_pretrained(abs_path)
|
| 175 |
+
resp = self.http.post(f"{self.base_url}/update_weights_from_disk", json={"model_path": abs_path}, timeout=self.timeout)
|
| 176 |
+
if resp.status_code != 200:
|
| 177 |
+
print(f"[SGLANG WARNING] update_weights 失败: {resp.status_code}, {resp.text}")
|
| 178 |
+
ok = resp.status_code == 200
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"[SGLANG WARNING] update_weights 异常: {e}")
|
| 181 |
+
ok = False
|
| 182 |
+
if dist.is_initialized():
|
| 183 |
+
ok_t = torch.tensor(int(ok), device=next(model.parameters()).device)
|
| 184 |
+
dist.broadcast(ok_t, src=0)
|
| 185 |
+
dist.barrier()
|
| 186 |
+
ok = bool(ok_t.item())
|
| 187 |
+
if not ok:
|
| 188 |
+
raise RuntimeError("SGLang update_policy failed")
|
| 189 |
+
return ok
|
| 190 |
+
|
| 191 |
+
def flush_cache(self) -> bool:
|
| 192 |
+
resp = self.http.post(f"{self.base_url}/flush_cache", timeout=30)
|
| 193 |
+
return resp.status_code == 200
|
| 194 |
+
|
| 195 |
+
def health(self) -> bool:
|
| 196 |
+
try:
|
| 197 |
+
resp = self.http.get(f"{self.base_url}/health", timeout=5)
|
| 198 |
+
return resp.status_code == 200
|
| 199 |
+
except Exception:
|
| 200 |
+
return False
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def create_rollout_engine(
|
| 204 |
+
engine_type: str = "torch",
|
| 205 |
+
policy_model: torch.nn.Module = None,
|
| 206 |
+
tokenizer=None,
|
| 207 |
+
device: str = "cuda",
|
| 208 |
+
autocast_ctx=None,
|
| 209 |
+
sglang_base_url: str = None,
|
| 210 |
+
sglang_model_path: str = None,
|
| 211 |
+
sglang_shared_path: str = None,
|
| 212 |
+
) -> RolloutEngine:
|
| 213 |
+
if engine_type == "torch":
|
| 214 |
+
return TorchRolloutEngine(policy_model, tokenizer, device, autocast_ctx)
|
| 215 |
+
elif engine_type == "sglang":
|
| 216 |
+
return SGLangRolloutEngine(sglang_base_url, sglang_model_path, sglang_shared_path)
|
| 217 |
+
else:
|
| 218 |
+
raise ValueError(f"不支持的引擎类型: {engine_type}")
|
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 注:不建议再重复训练tokenizer(“词典”),MiniMind已自带,此脚本仅供学习和参考。基于不同词典训练的模型将导致输出完全不统一,降低社区的模型复用性
|
| 2 |
+
# Note: It is not recommended to re-train the tokenizer. MiniMind already includes one. This script is for learning and reference only. Training models with different tokenizers will lead to inconsistent outputs and reduce model reusability in the community.
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
from tokenizers import decoders, models, pre_tokenizers, trainers, Tokenizer
|
| 6 |
+
|
| 7 |
+
DATA_PATH = '../dataset/sft_t2t_mini.jsonl'
|
| 8 |
+
TOKENIZER_DIR = '../model_learn_tokenizer/'
|
| 9 |
+
VOCAB_SIZE = 6400
|
| 10 |
+
SPECIAL_TOKENS_NUM = 36
|
| 11 |
+
|
| 12 |
+
def get_texts(data_path):
|
| 13 |
+
with open(data_path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 14 |
+
for i, line in enumerate(f):
|
| 15 |
+
if i >= 10000: break # 选10000行测试
|
| 16 |
+
try:
|
| 17 |
+
data = json.loads(line)
|
| 18 |
+
contents = [item.get('content') for item in data.get('conversations', []) if item.get('content')]
|
| 19 |
+
if contents:
|
| 20 |
+
yield "\n".join(contents)
|
| 21 |
+
except json.JSONDecodeError:
|
| 22 |
+
continue
|
| 23 |
+
|
| 24 |
+
def train_tokenizer(data_path, tokenizer_dir, vocab_size, special_tokens_num=SPECIAL_TOKENS_NUM):
|
| 25 |
+
tokenizer = Tokenizer(models.BPE())
|
| 26 |
+
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
|
| 27 |
+
|
| 28 |
+
special_tokens_list = [
|
| 29 |
+
"<|endoftext|>", "<|im_start|>", "<|im_end|>",
|
| 30 |
+
"<|object_ref_start|>", "<|object_ref_end|>", "<|box_start|>", "<|box_end|>", "<|quad_start|>", "<|quad_end|>",
|
| 31 |
+
"<|vision_start|>", "<|vision_end|>", "<|vision_pad|>", "<|image_pad|>", "<|video_pad|>",
|
| 32 |
+
"<|audio_start|>", "<|audio_end|>", "<|audio_pad|>", "<tts_pad>", "<tts_text_bos>", "<tts_text_eod>", "<tts_text_bos_single>"
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
additional_tokens_list = [
|
| 36 |
+
"<tool_call>", "</tool_call>",
|
| 37 |
+
"<tool_response>", "</tool_response>",
|
| 38 |
+
"<think>", "</think>"
|
| 39 |
+
]
|
| 40 |
+
num_buffer = special_tokens_num - len(special_tokens_list + additional_tokens_list)
|
| 41 |
+
buffer_tokens = [f"<|buffer{i}|>" for i in range(1, num_buffer + 1)] # 预留一定数量的token位置
|
| 42 |
+
all_special_tokens = special_tokens_list + additional_tokens_list + buffer_tokens
|
| 43 |
+
trainer = trainers.BpeTrainer(
|
| 44 |
+
vocab_size=vocab_size,
|
| 45 |
+
show_progress=True,
|
| 46 |
+
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
|
| 47 |
+
special_tokens=all_special_tokens
|
| 48 |
+
)
|
| 49 |
+
texts = get_texts(data_path)
|
| 50 |
+
tokenizer.train_from_iterator(texts, trainer=trainer)
|
| 51 |
+
tokenizer.decoder = decoders.ByteLevel()
|
| 52 |
+
tokenizer.add_special_tokens(special_tokens_list)
|
| 53 |
+
|
| 54 |
+
os.makedirs(tokenizer_dir, exist_ok=True)
|
| 55 |
+
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
| 56 |
+
tokenizer.model.save(tokenizer_dir)
|
| 57 |
+
tokenizer_json_path = os.path.join(tokenizer_dir, "tokenizer.json")
|
| 58 |
+
with open(tokenizer_json_path, 'r', encoding='utf-8') as f:
|
| 59 |
+
tokenizer_data = json.load(f)
|
| 60 |
+
for token_info in tokenizer_data.get('added_tokens', []):
|
| 61 |
+
if token_info['content'] not in special_tokens_list:
|
| 62 |
+
token_info['special'] = False
|
| 63 |
+
with open(tokenizer_json_path, 'w', encoding='utf-8') as f:
|
| 64 |
+
json.dump(tokenizer_data, f, ensure_ascii=False, indent=2)
|
| 65 |
+
|
| 66 |
+
added_tokens_decoder = {}
|
| 67 |
+
for i, token in enumerate(all_special_tokens):
|
| 68 |
+
idx = tokenizer.token_to_id(token)
|
| 69 |
+
added_tokens_decoder[str(idx)] = {
|
| 70 |
+
"content": token,
|
| 71 |
+
"lstrip": False,
|
| 72 |
+
"normalized": False,
|
| 73 |
+
"rstrip": False,
|
| 74 |
+
"single_word": False,
|
| 75 |
+
"special": True if token in special_tokens_list else False
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
config = {
|
| 79 |
+
"add_bos_token": False,
|
| 80 |
+
"add_eos_token": False,
|
| 81 |
+
"add_prefix_space": False,
|
| 82 |
+
"added_tokens_decoder": added_tokens_decoder,
|
| 83 |
+
"additional_special_tokens": [t for t in special_tokens_list if t not in ["<|endoftext|>"]],
|
| 84 |
+
"bos_token": "<|im_start|>",
|
| 85 |
+
"clean_up_tokenization_spaces": False,
|
| 86 |
+
"eos_token": "<|im_end|>",
|
| 87 |
+
"legacy": True,
|
| 88 |
+
"model_max_length": 131072,
|
| 89 |
+
"pad_token": "<|endoftext|>",
|
| 90 |
+
"sp_model_kwargs": {},
|
| 91 |
+
"spaces_between_special_tokens": False,
|
| 92 |
+
"unk_token": "<|endoftext|>",
|
| 93 |
+
"image_token": "<|image_pad|>",
|
| 94 |
+
"audio_token": "<|audio_pad|>",
|
| 95 |
+
"video_token": "<|video_pad|>",
|
| 96 |
+
"vision_bos_token": "<|vision_start|>",
|
| 97 |
+
"vision_eos_token": "<|vision_end|>",
|
| 98 |
+
"audio_bos_token": "<|audio_start|>",
|
| 99 |
+
"audio_eos_token": "<|audio_end|>",
|
| 100 |
+
"chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if message.content is string %}\n {%- set content = message.content %}\n {%- else %}\n {%- set content = '' %}\n {%- endif %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if true %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if open_thinking is defined and open_thinking is true %}\n {{- '<think>\\n' }}\n {%- else %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}\n{%- endif %}",
|
| 101 |
+
"tokenizer_class": "PreTrainedTokenizerFast"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w", encoding="utf-8") as f:
|
| 105 |
+
json.dump(config, f, ensure_ascii=False, indent=4)
|
| 106 |
+
print("Tokenizer training completed.")
|
| 107 |
+
|
| 108 |
+
def eval_tokenizer(tokenizer_dir):
|
| 109 |
+
from transformers import AutoTokenizer
|
| 110 |
+
tokenizer = AutoTokenizer.from_pretrained(tokenizer_dir)
|
| 111 |
+
messages = [
|
| 112 |
+
{"role": "system", "content": "你是一个优秀的聊天机器人,总是给我正确的回应!"},
|
| 113 |
+
{"role": "user", "content": '你来自哪里?'},
|
| 114 |
+
{"role": "assistant", "content": '我来自月球'},
|
| 115 |
+
{"role": "user", "content": '你到底来自哪里?'},
|
| 116 |
+
{"role": "assistant", "content": '我来自地球'}
|
| 117 |
+
]
|
| 118 |
+
new_prompt = tokenizer.apply_chat_template(
|
| 119 |
+
messages,
|
| 120 |
+
tokenize=False
|
| 121 |
+
)
|
| 122 |
+
print('-'*100)
|
| 123 |
+
print(new_prompt)
|
| 124 |
+
print('-'*100)
|
| 125 |
+
print('tokenizer词表长度:', len(tokenizer))
|
| 126 |
+
model_inputs = tokenizer(new_prompt)
|
| 127 |
+
print('encoder长度:', len(model_inputs['input_ids']))
|
| 128 |
+
response = tokenizer.decode(model_inputs['input_ids'], skip_special_tokens=False)
|
| 129 |
+
print('decoder一致性:', response == new_prompt, "\n")
|
| 130 |
+
print('-'*100)
|
| 131 |
+
print('压缩率测试(Chars/Tokens):')
|
| 132 |
+
test_texts = [
|
| 133 |
+
# 中文样本 (约200字)
|
| 134 |
+
"人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器,该领域的研究包括机器人、语言识别、图像识别、自然语言处理和专家系统等。人工智能从诞生以来,理论和技术日益成熟,应用领域也不断扩大,可以设想,未来人工智能带来的科技产品,将会是人类智慧的“容器”。人工智能可以对人的意识、思维的信息过程的模拟。人工智能不是人的智能,但能像人那样思考、也可能超过人的智能。",
|
| 135 |
+
"星际航行是指在星系内甚至星系间的空间中进行的航行。由于宇宙空间极其广阔,传统的化学火箭动力在恒星间航行时显得力不从心。科学家们提出了多种方案,包括离子推进器、核热火箭、甚至是利用反物质作为能源的设想。此外,曲率驱动和虫洞旅行等科幻概念也在理论物理研究中被反复探讨。尽管目前人类的足迹仅限于月球,但随着核聚变技术和材料科学的突破,前往火星乃至更遥远的太阳系边缘将成为可能。",
|
| 136 |
+
# 英文样本 (约200词/字符)
|
| 137 |
+
"Large language models (LLMs) are a type of artificial intelligence (AI) trained on vast amounts of text data to understand and generate human-like language. These models use deep learning techniques, specifically transformers, to process and predict the next word in a sequence. LLMs like GPT-4, Llama, and Claude have demonstrated remarkable capabilities in coding, translation, and creative writing. However, they also face challenges such as hallucinations, where the model generates factually incorrect information, and the need for significant computational resources.",
|
| 138 |
+
"The development of sustainable energy is crucial for the future of our planet. As climate change continues to impact global weather patterns, transitioning from fossil fuels to renewable sources like solar, wind, and hydroelectric power has become an urgent priority. Innovations in battery storage technology and smart grid management are essential to ensure a reliable energy supply. International cooperation and policy frameworks are also necessary to drive the global shift towards a greener economy and reduce carbon emissions.",
|
| 139 |
+
# 混合样本
|
| 140 |
+
"Python 是一种高级编程语言,以其简洁的语法和强大的生态系统而闻名。It is widely used in data science, machine learning, and web development. 开发者可以利用 NumPy, Pandas, and PyTorch 等库快速构建复杂的应用。学习 Python 的过程非常愉快,因为它的代码读起来就像英语一样。Whether you are a beginner or an expert, Python offers something for everyone.",
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
total_compression = 0
|
| 144 |
+
for i, text in enumerate(test_texts):
|
| 145 |
+
encoded = tokenizer.encode(text)
|
| 146 |
+
token_count = len(encoded)
|
| 147 |
+
char_count = len(text)
|
| 148 |
+
compression_ratio = char_count / token_count
|
| 149 |
+
total_compression += compression_ratio
|
| 150 |
+
print(f"样本 {i+1} | 字符数: {char_count:4} | Tokens: {token_count:3} | 压缩率: {compression_ratio:.2f}")
|
| 151 |
+
|
| 152 |
+
print(f"平均压缩率: {total_compression / len(test_texts):.2f}")
|
| 153 |
+
print('-'*100)
|
| 154 |
+
print('流式解码(字节缓冲)测试:')
|
| 155 |
+
input_ids = model_inputs['input_ids']
|
| 156 |
+
token_cache = []
|
| 157 |
+
for tid in input_ids:
|
| 158 |
+
token_cache.append(tid)
|
| 159 |
+
current_decode = tokenizer.decode(token_cache)
|
| 160 |
+
if current_decode and '\ufffd' not in current_decode:
|
| 161 |
+
display_ids = token_cache[0] if len(token_cache) == 1 else token_cache
|
| 162 |
+
raw_tokens = [tokenizer.convert_ids_to_tokens(int(t)) for t in (token_cache if isinstance(token_cache, list) else [token_cache])]
|
| 163 |
+
print(f'Token ID: {str(display_ids):15} -> Raw: {str(raw_tokens):20} -> Decode Str: {current_decode}')
|
| 164 |
+
token_cache = []
|
| 165 |
+
|
| 166 |
+
if __name__ == '__main__':
|
| 167 |
+
train_tokenizer(DATA_PATH, TOKENIZER_DIR, VOCAB_SIZE)
|
| 168 |
+
eval_tokenizer(TOKENIZER_DIR)
|