Compare commits

...

17 Commits

Author SHA1 Message Date
AmirX0 05569d20ab
Merge 56a21ed3c2 into 1e6e909f88 2026-09-20 20:57:45 +03:30
jingyaogong 1e6e909f88 [update] trim redundant comments 2026-09-20 20:33:40 +08:00
jingyaogong b00be3e5ca
Merge pull request #769 from qizwiz/fix/llm-response-unguarded
fix: guard against empty/filtered LLM responses in chat_api.py
2026-09-20 20:19:39 +08:00
jingyaogong 7f5ba40a4e
Merge pull request #821 from lycDragonSlayer/refactor/remove-redundant-update-policy
[refactor] remove redundant rollout update_policy after torch.compile
2026-09-20 20:13:29 +08:00
jingyaogong 949488fdb4
Merge pull request #829 from DaoyuanLi2816/fix/dpo-paired-think-format
[fix] keep DPO preference-pair think formatting aligned
2026-09-20 20:07:46 +08:00
jingyaogong e41bf7cacf
Merge pull request #863 from Linxiushen/fix-fp16-attention-mask-nan
[fix] float16 下 attention mask 的 -1e9 溢出成 -inf,导致整条序列输出 NaN
2026-09-20 19:49:22 +08:00
jingyaogong b330a4d5c0
Merge pull request #860 from Linxiushen/fix-tokenizer-pretrain-format
[fix] train_tokenizer 读预训练语料时产出 0 条文本,静默训出空词表
2026-09-20 19:47:52 +08:00
jingyaogong b956bad49f
Merge pull request #865 from Linxiushen/fix-requirements-missing-fastapi
[fix] requirements.txt 缺 fastapi/uvicorn,按 README 跑 serve_openai_api.py 直接崩
2026-09-20 19:42:22 +08:00
jingyaogong d698c06fc5
Merge pull request #864 from Linxiushen/fix-nonstream-max-tokens
[fix] serve_openai_api 非流式端点的 max_tokens 不生效,与流式端点行为不一致
2026-09-20 19:42:05 +08:00
Linxiushen 984a2c3611 [fix] requirements.txt 缺 fastapi/uvicorn,按 README 跑 serve_openai_api.py 直接崩
README 的「第0步」让用户 pip install -r requirements.txt 完成环境搭建,
随后在 OpenAI 兼容服务一节给出可直接运行的命令(README.md:1652):

    cd scripts && python serve_openai_api.py

但 scripts/serve_openai_api.py 第12行 import uvicorn、第16-17行
from fastapi import ... 都是顶层硬依赖,而 requirements.txt 里两个都没有。

复现(venv 严格按 requirements.txt 装齐):
    $ cd scripts && python serve_openai_api.py --help
    ModuleNotFoundError: No module named 'uvicorn'

补上两行。版本已在独立 venv 里验证可与已声明的 pydantic==2.11.5 共存。
2026-09-20 11:57:27 +08:00
Linxiushen d4b67f1783 [fix] serve_openai_api 非流式端点的 max_tokens 不生效,与流式端点行为不一致
非流式分支传的是 max_length,但 MiniMindForCausalLM.generate() 的签名里
没有这个形参(只有 max_new_tokens=8192),max_length 被 **kwargs 吞掉后
无人处理,于是 request.max_tokens 对生成长度零约束。

同一文件的流式分支(第117行)传的是 max_new_tokens=max_tokens,精确生效。
同一个请求参数在两条路径上行为完全不同。

判决性实验:同时传 max_length=15 与 max_new_tokens=20,
prompt 长度 10 -> 实际输出总长 30(新增 20),证明 max_length 被无视。

改为与流式分支一致的 max_new_tokens=request.max_tokens(一行)。
2026-09-20 11:54:35 +08:00
Linxiushen ba10c4d17b [fix] float16 下 attention mask 的 -1e9 溢出成 -inf,导致整条序列输出 NaN
Attention.forward 的手写 softmax 分支(flash SDPA 关闭时走这里):

    scores = (xq @ xk.transpose(-2, -1)) / math.sqrt(self.head_dim)
    ...
    if attention_mask is not None: scores += (1.0 - mask) * -1e9
    output = ... F.softmax(scores.float(), dim=-1) ...

scores 的 dtype 跟随 xq/xk。float16 模型下它是 float16,而
torch.finfo(torch.float16).min ≈ -65504,把 -1e9 累加进去会饱和成 -inf
(不是一个很大的有限负数)。当某个 query 在因果范围内可见的 key 全被 mask
掉时(左 padding 必然出现),softmax 整行 = NaN,并在下一层通过 K/V 投影
扩散到同序列里 mask=1 的有效 token 上。

.float() 加在 softmax 那一刻已经太晚——溢出发生在它之前。

修复:把 float32 提升提前到加 mask 之前,两处共 2 行。
fp32/bf16 路径数值等价(fp32 下原来的 .float() 本就是 no-op)。
2026-09-20 11:50:28 +08:00
Linxiushen cb08b9300b [fix] train_tokenizer 读预训练语料时产出 0 条文本,静默训出空词表
get_texts() 只解析 data.get('conversations'),但主线预训练数据
pretrain_t2t(_mini).jsonl 的每行是 {"text": ...}(README「数据集下载」一节)。
把 DATA_PATH 指向预训练语料时,生成器一条文本都不产出,BpeTrainer 仅基于
initial_alphabet 训练,得到一个没有任何 merge 的词表,全程不报错。

复现(改 DATA_PATH 为任意 pretrain_t2t*.jsonl,或直接调用):

    from train_tokenizer import get_texts
    sum(1 for _ in get_texts('../dataset/pretrain_t2t_mini.jsonl'))
    # 修复前: 0
    # 修复后: 1270238

改动:
- get_texts 同时兼容 {"text": ...} 与 {"conversations": [...]} 两种格式
- 产出 0 条时 raise ValueError,而不是让调用方拿到一个空词表
- 把硬编码的 `if i >= 10000: break` 提为模块级 MAX_LINES(默认 0 = 全量)。
  原先这一行也会让 SFT 语料只用前 10000 行,且无从关闭;
  另外它按「行号」计数而非「有效文本条数」,遇到空内容或解析失败的行会提前耗尽配额。

验证:用 pretrain_t2t 的前 3000 篇跑 train_tokenizer(vocab_size=2000),
得到 vocab=2000 / merges=1708;修复前同样输入得到 0 条 merge。
两种格式各 20000 行的合成数据上,产出条数均为 20000(修复前分别是 0 和 10000)。
2026-09-20 10:33:51 +08:00
AmirX0 56a21ed3c2 docs: expand bilingual dataset guide 2026-09-01 07:06:36 +03:30
Daoyuan Li 56d4cddfd8 [fix] keep DPO preference pair think formatting aligned 2026-08-29 18:29:00 -07:00
TOPSAIL\78190 0c91dea4a2 [refactor] remove redundant rollout update_policy after torch.compile 2026-08-24 12:43:50 +08:00
Jonathan Hill 182bff1bd8 fix: guard against empty/filtered LLM responses in chat_api.py
Add null check before accessing choices[0].message.content in the
non-streaming path. An empty choices list causes IndexError; a None
message (returned by some providers on content-filtered responses
with HTTP 200) causes AttributeError.
2026-05-18 03:44:31 -05:00
11 changed files with 265 additions and 31 deletions

View File

@ -486,6 +486,8 @@ torchrun --nproc_per_node N train_xxx.py
MiniMind训练数据集下载地址: [ModelScope](https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files) | [HuggingFace](https://huggingface.co/datasets/jingyaogong/minimind_dataset/tree/main) MiniMind训练数据集下载地址: [ModelScope](https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files) | [HuggingFace](https://huggingface.co/datasets/jingyaogong/minimind_dataset/tree/main)
下载、文件用途、最终格式与自定义数据检查清单可参考[数据集双语速查指南](./dataset/dataset.md)。
> 无需全部clone,可单独下载所需的文件 > 无需全部clone,可单独下载所需的文件
将下载的数据集文件放到`./dataset/`目录下(✨为推荐的必须项) 将下载的数据集文件放到`./dataset/`目录下(✨为推荐的必须项)

View File

@ -485,6 +485,8 @@ Besides this, other RL data maintains the same format as SFT data, typically fil
MiniMind training dataset download links: [ModelScope](https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files) | [HuggingFace](https://huggingface.co/datasets/jingyaogong/minimind_dataset/tree/main) MiniMind training dataset download links: [ModelScope](https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files) | [HuggingFace](https://huggingface.co/datasets/jingyaogong/minimind_dataset/tree/main)
For downloads, file-to-trainer mapping, final schemas, and a custom-data checklist, see the [bilingual dataset quick reference](./dataset/dataset.md).
> No need to clone everything, you can download individual files as needed > No need to clone everything, you can download individual files as needed
Place the downloaded dataset files in the `./dataset/` directory (✨ indicates recommended essentials) Place the downloaded dataset files in the `./dataset/` directory (✨ indicates recommended essentials)

View File

@ -1,5 +1,219 @@
# MiniMind Datasets # MiniMind 数据集指南 / Dataset Guide
将所有下载的数据集文件放置到当前目录. [中文](#user-content-chinese) | [English](#user-content-english)
Place the downloaded dataset file in the current directory. <a id="chinese"></a>
## 中文
本目录存放 MiniMind 各训练阶段使用的最终 JSONL 文件。官方发布的数据已经完成收集、蒸馏、清洗、去重、长度控制与格式统一,可直接由 `dataset/lm_dataset.py` 中的数据加载器读取。
### 下载与最小复现
数据集可从 [ModelScope](https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files) 或 [Hugging Face](https://huggingface.co/datasets/jingyaogong/minimind_dataset/tree/main) 按文件下载,无需克隆全部数据。
将文件放在当前目录:
```text
dataset/
├── pretrain_t2t_mini.jsonl
├── sft_t2t_mini.jsonl
├── pretrain_t2t.jsonl
├── sft_t2t.jsonl
├── dpo.jsonl
├── rlaif.jsonl
├── agent_rl.jsonl
└── agent_rl_math.jsonl
```
只想快速跑通从预训练到对话模型的流程时,下载以下两个文件即可:
- `pretrain_t2t_mini.jsonl`
- `sft_t2t_mini.jsonl`
然后从仓库根目录运行:
```bash
cd trainer
python train_pretrain.py
python train_full_sft.py
```
### 文件与训练脚本对应关系
| 文件 | 用途 | 默认使用者 |
| --- | --- | --- |
| `pretrain_t2t_mini.jsonl` | 快速预训练 | `train_pretrain.py` |
| `pretrain_t2t.jsonl` | 完整 MiniMind-3 预训练 | 通过 `train_pretrain.py --data_path ...` 指定 |
| `sft_t2t_mini.jsonl` | 快速 SFT、蒸馏及 LoRA 自定义数据格式参考 | `train_full_sft.py`、`train_distillation.py` |
| `sft_t2t.jsonl` | 完整 MiniMind-3 SFT | 通过 `train_full_sft.py --data_path ...` 指定 |
| `dpo.jsonl` | 偏好优化 | `train_dpo.py` |
| `rlaif.jsonl` | PPO、GRPO 与 CISPO | `train_ppo.py`、`train_grpo.py` |
| `agent_rl.jsonl` | 多轮 Tool-Use Agentic RL | `train_agent.py` |
| `agent_rl_math.jsonl` | 带可验证最终答案的数学 Agentic RL | 通过 `train_agent.py --data_path ...` 指定 |
上表中的训练脚本都支持 `--data_path`,因此也可直接指向自己的 JSONL 文件。
### 最终数据格式
预训练数据每行包含一个 `text` 字段:
```jsonl
{"text": "Transformer 通过自注意力机制建模上下文关系。"}
```
SFT、RLAIF 与常规对话数据使用 `conversations` 数组:
```json
{
"conversations": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!"}
]
}
```
DPO 数据分别提供偏好回复与非偏好回复:
```json
{
"chosen": [
{"role": "user", "content": "Q"},
{"role": "assistant", "content": "good answer"}
],
"rejected": [
{"role": "user", "content": "Q"},
{"role": "assistant", "content": "bad answer"}
]
}
```
Agentic RL 数据还可在 system 消息中包含序列化的 `tools`,并用顶层 `gt` 字段提供可验证目标。具体解析逻辑以 [`lm_dataset.py`](./lm_dataset.py) 为准。
### 来源、处理与自定义数据
当前公开版本使用的主要社区来源包括:
- [匠数大模型数据集](https://www.modelscope.cn/datasets/deepctrl/deepctrl-sft-data)
- [Magpie-Align](https://www.modelscope.cn/organization/Magpie-Align)
- [R1-Distill-SFT](https://www.modelscope.cn/datasets/AI-ModelScope/R1-Distill-SFT)
- [COIG](https://huggingface.co/datasets/BAAI/COIG)
- [Step-3.5-Flash-SFT](https://huggingface.co/datasets/stepfun-ai/Step-3.5-Flash-SFT)
- [DPO-En-Zh-20k](https://huggingface.co/datasets/llamafactory/DPO-En-Zh-20k)
本仓库目前提供训练所需的最终数据、格式说明和加载器;原始数据的完整收集、蒸馏与清洗流水线并未作为可执行脚本包含在本目录中。准备自定义数据时应:
1. 确认每个来源允许当前用途与再分发方式,并保留来源和许可证记录。
2. 完成去重、质量过滤、隐私信息清理与长度控制。
3. 转换为上面的最终 JSONL 格式。
4. 先用小样本运行对应训练脚本,确认 schema 和 chat template 正确,再开始完整训练。
数据来源、推荐组合、Tool Calling 格式与 token 长度说明以仓库根目录的[中文 README](../README.md#-数据介绍)为准。
<a id="english"></a>
## English
This directory stores the final JSONL files consumed by each MiniMind training stage. The published datasets have already gone through collection, distillation, cleaning, deduplication, length control, and format normalization, and can be read directly by the loaders in `dataset/lm_dataset.py`.
### Download and minimal reproduction
Download individual files from [ModelScope](https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files) or [Hugging Face](https://huggingface.co/datasets/jingyaogong/minimind_dataset/tree/main). You do not need to clone the full dataset repository.
Place the downloaded files in this directory:
```text
dataset/
├── pretrain_t2t_mini.jsonl
├── sft_t2t_mini.jsonl
├── pretrain_t2t.jsonl
├── sft_t2t.jsonl
├── dpo.jsonl
├── rlaif.jsonl
├── agent_rl.jsonl
└── agent_rl_math.jsonl
```
For the quickest pretraining-to-chat-model reproduction, only these two files are required:
- `pretrain_t2t_mini.jsonl`
- `sft_t2t_mini.jsonl`
Then run from the repository root:
```bash
cd trainer
python train_pretrain.py
python train_full_sft.py
```
### Dataset-to-trainer map
| File | Purpose | Default consumer |
| --- | --- | --- |
| `pretrain_t2t_mini.jsonl` | Quick pretraining | `train_pretrain.py` |
| `pretrain_t2t.jsonl` | Full MiniMind-3 pretraining | Pass with `train_pretrain.py --data_path ...` |
| `sft_t2t_mini.jsonl` | Quick SFT, distillation, and the reference schema for custom LoRA data | `train_full_sft.py`, `train_distillation.py` |
| `sft_t2t.jsonl` | Full MiniMind-3 SFT | Pass with `train_full_sft.py --data_path ...` |
| `dpo.jsonl` | Preference optimization | `train_dpo.py` |
| `rlaif.jsonl` | PPO, GRPO, and CISPO | `train_ppo.py`, `train_grpo.py` |
| `agent_rl.jsonl` | Multi-turn Tool-Use Agentic RL | `train_agent.py` |
| `agent_rl_math.jsonl` | Math Agentic RL with verifiable final answers | Pass with `train_agent.py --data_path ...` |
Every trainer listed above accepts `--data_path`, so it can also read a custom JSONL file directly.
### Final data schemas
Each pretraining line contains a `text` field:
```jsonl
{"text": "Transformers model context with self-attention."}
```
SFT, RLAIF, and regular conversation data use a `conversations` array:
```json
{
"conversations": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hello!"}
]
}
```
DPO data provides preferred and rejected conversations separately:
```json
{
"chosen": [
{"role": "user", "content": "Q"},
{"role": "assistant", "content": "good answer"}
],
"rejected": [
{"role": "user", "content": "Q"},
{"role": "assistant", "content": "bad answer"}
]
}
```
Agentic RL records may also contain serialized `tools` on the system message and a top-level `gt` field for verifiable targets. See [`lm_dataset.py`](./lm_dataset.py) for the authoritative parsing behavior.
### Sources, processing, and custom data
Major community sources used by the current published datasets include:
- [Craftsman LLM Dataset](https://www.modelscope.cn/datasets/deepctrl/deepctrl-sft-data)
- [Magpie-Align](https://www.modelscope.cn/organization/Magpie-Align)
- [R1-Distill-SFT](https://www.modelscope.cn/datasets/AI-ModelScope/R1-Distill-SFT)
- [COIG](https://huggingface.co/datasets/BAAI/COIG)
- [Step-3.5-Flash-SFT](https://huggingface.co/datasets/stepfun-ai/Step-3.5-Flash-SFT)
- [DPO-En-Zh-20k](https://huggingface.co/datasets/llamafactory/DPO-En-Zh-20k)
This repository currently provides the final training datasets, their schemas, and the loaders. The complete raw-data collection, distillation, and cleaning pipeline is not included here as an executable script. When preparing custom data:
1. Confirm that each source permits the intended use and redistribution, and retain provenance and license records.
2. Deduplicate, quality-filter, remove private information, and control sample lengths.
3. Convert records to the final JSONL schemas above.
4. Run the matching trainer on a small sample to validate the schema and chat template before starting a full training run.
For source details, recommended dataset combinations, Tool Calling schemas, and token-length guidance, see the [English README](../README_en.md#-data-introduction).

View File

@ -28,10 +28,13 @@ def pre_processing_chat(conversations, add_system_ratio=0.2):
return [{'role': 'system', 'content': random.choice(SYSTEM_PROMPTS)}] + conversations return [{'role': 'system', 'content': random.choice(SYSTEM_PROMPTS)}] + conversations
return conversations return conversations
def post_processing_chat(prompt_content, empty_think_ratio=0.2): def post_processing_chat(prompt_content, empty_think_ratio=0.2, remove_empty_think=None):
# 以80%概率移除空思考标签 # 以80%概率移除空思考标签
if '<think>\n\n</think>\n\n' in prompt_content and random.random() > empty_think_ratio: if '<think>\n\n</think>\n\n' in prompt_content:
prompt_content = prompt_content.replace('<think>\n\n</think>\n\n', '') if remove_empty_think is None:
remove_empty_think = random.random() > empty_think_ratio
if remove_empty_think:
prompt_content = prompt_content.replace('<think>\n\n</think>\n\n', '')
return prompt_content return prompt_content
class PretrainDataset(Dataset): class PretrainDataset(Dataset):
@ -139,12 +142,13 @@ class DPODataset(Dataset):
chosen_prompt = self.tokenizer.apply_chat_template( chosen_prompt = self.tokenizer.apply_chat_template(
chosen, tokenize=False, add_generation_prompt=False chosen, tokenize=False, add_generation_prompt=False
) )
chosen_prompt = post_processing_chat(chosen_prompt)
rejected_prompt = self.tokenizer.apply_chat_template( rejected_prompt = self.tokenizer.apply_chat_template(
rejected, tokenize=False, add_generation_prompt=False rejected, tokenize=False, add_generation_prompt=False
) )
rejected_prompt = post_processing_chat(rejected_prompt) remove_empty_think = random.random() > 0.2
chosen_prompt = post_processing_chat(chosen_prompt, remove_empty_think=remove_empty_think)
rejected_prompt = post_processing_chat(rejected_prompt, remove_empty_think=remove_empty_think)
chosen_encoding = self.tokenizer( chosen_encoding = self.tokenizer(
chosen_prompt, truncation=True, max_length=self.max_length, padding='max_length' chosen_prompt, truncation=True, max_length=self.max_length, padding='max_length'
) )
@ -253,4 +257,4 @@ class AgentRLDataset(Dataset):
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@ -125,10 +125,10 @@ class Attention(nn.Module):
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)): 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)):
output = F.scaled_dot_product_attention(xq, xk, xv, dropout_p=self.dropout if self.training else 0.0, is_causal=self.is_causal) output = F.scaled_dot_product_attention(xq, xk, xv, dropout_p=self.dropout if self.training else 0.0, is_causal=self.is_causal)
else: else:
scores = (xq @ xk.transpose(-2, -1)) / math.sqrt(self.head_dim) scores = (xq @ xk.transpose(-2, -1)).float() / math.sqrt(self.head_dim)
if self.is_causal: scores[:, :, :, -seq_len:] += torch.full((seq_len, seq_len), float("-inf"), device=scores.device).triu(1) if self.is_causal: scores[:, :, :, -seq_len:] += torch.full((seq_len, seq_len), float("-inf"), device=scores.device).triu(1)
if attention_mask is not None: scores += (1.0 - attention_mask.unsqueeze(1).unsqueeze(2)) * -1e9 if attention_mask is not None: scores += (1.0 - attention_mask.unsqueeze(1).unsqueeze(2)) * -1e9
output = self.attn_dropout(F.softmax(scores.float(), dim=-1).type_as(xq)) @ xv output = self.attn_dropout(F.softmax(scores, dim=-1).type_as(xq)) @ xv
output = output.transpose(1, 2).reshape(bsz, seq_len, -1) output = output.transpose(1, 2).reshape(bsz, seq_len, -1)
output = self.resid_dropout(self.o_proj(output)) output = self.resid_dropout(self.o_proj(output))
return output, past_kv return output, past_kv

View File

@ -28,6 +28,8 @@ streamlit==1.50.0
einops==0.8.1 einops==0.8.1
swanlab==0.9.8 swanlab==0.9.8
modelscope==1.37.0 modelscope==1.37.0
fastapi==0.120.1
uvicorn==0.38.0
# torch==2.6.0 # torch==2.6.0
# torchvision==0.21.0 # torchvision==0.21.0
# peft==0.7.1 # peft==0.7.1

View File

@ -202,7 +202,7 @@ async def chat_completions(request: ChatRequest):
with torch.no_grad(): with torch.no_grad():
generated_ids = model.generate( generated_ids = model.generate(
inputs["input_ids"], inputs["input_ids"],
max_length=inputs["input_ids"].shape[1] + request.max_tokens, max_new_tokens=request.max_tokens,
do_sample=True, do_sample=True,
attention_mask=inputs["attention_mask"], attention_mask=inputs["attention_mask"],
pad_token_id=tokenizer.pad_token_id, pad_token_id=tokenizer.pad_token_id,

View File

@ -273,8 +273,6 @@ def rl_train_epoch(epoch, loader, iters, rollout_engine, ref_model, reward_model
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) 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)
with autocast_ctx: with autocast_ctx:
# 反向传播必须经过 DDP 包装后的模块:直接调用 .module 会跳过
# DDP 的 prepare_for_backward,梯度不会 all-reduce,各卡静默发散。
res = model(input_ids, attention_mask=full_mask) res = model(input_ids, attention_mask=full_mask)
aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device) aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device)
logits = res.logits[:, :-1, :] logits = res.logits[:, :-1, :]
@ -471,7 +469,6 @@ if __name__ == "__main__":
if args.use_compile == 1: if args.use_compile == 1:
model = torch.compile(model) model = torch.compile(model)
Logger('torch.compile enabled') Logger('torch.compile enabled')
rollout_engine.update_policy(model)
if dist.is_initialized(): if dist.is_initialized():
# 同 train_ppo:RoPE buffer 各 rank 一致,每步广播纯属浪费 # 同 train_ppo:RoPE buffer 各 rank 一致,每步广播纯属浪费
model = DistributedDataParallel(model, device_ids=[local_rank], broadcast_buffers=False) model = DistributedDataParallel(model, device_ids=[local_rank], broadcast_buffers=False)

View File

@ -96,8 +96,6 @@ def grpo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, reward_mod
rewards = calculate_rewards(prompts, completions, reward_model).to(args.device) # [B*num_gen] rewards = calculate_rewards(prompts, completions, reward_model).to(args.device) # [B*num_gen]
with autocast_ctx: with autocast_ctx:
# 反向传播必须经过 DDP 包装后的模块:直接调用 .module 会跳过
# DDP 的 prepare_for_backward,梯度不会 all-reduce,各卡静默发散。
res = model(outputs, attention_mask=full_mask) res = model(outputs, attention_mask=full_mask)
aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device) aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device)
per_token_logps = F.log_softmax(res.logits[:, :-1, :], dim=-1).gather(2, outputs[:, 1:].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos) per_token_logps = F.log_softmax(res.logits[:, :-1, :], dim=-1).gather(2, outputs[:, 1:].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos)
@ -312,7 +310,6 @@ if __name__ == "__main__":
if args.use_compile == 1: if args.use_compile == 1:
model = torch.compile(model) model = torch.compile(model)
Logger('torch.compile enabled') Logger('torch.compile enabled')
rollout_engine.update_policy(model)
if dist.is_initialized(): if dist.is_initialized():
# 同 train_ppo:RoPE buffer 各 rank 一致,每步广播纯属浪费 # 同 train_ppo:RoPE buffer 各 rank 一致,每步广播纯属浪费
model = DistributedDataParallel(model, device_ids=[local_rank], broadcast_buffers=False) model = DistributedDataParallel(model, device_ids=[local_rank], broadcast_buffers=False)

View File

@ -166,16 +166,12 @@ def ppo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, actor_sched
for i in range(0, B, mb_size): for i in range(0, B, mb_size):
inds = b_inds[i:i + mb_size] inds = b_inds[i:i + mb_size]
# 反向传播必须经过 DDP 包装后的模块:直接调用 .module 会跳过
# DDP 的 prepare_for_backward,梯度不会 all-reduce,各卡静默发散。
mb_values_seq = critic_model(input_ids=gen_out[inds], attention_mask=full_mask[inds]) mb_values_seq = critic_model(input_ids=gen_out[inds], attention_mask=full_mask[inds])
mb_resp_values = mb_values_seq.gather(1, logp_pos[inds]) mb_resp_values = mb_values_seq.gather(1, logp_pos[inds])
with autocast_ctx: with autocast_ctx:
res = actor_model(input_ids=gen_out[inds], attention_mask=full_mask[inds]) res = actor_model(input_ids=gen_out[inds], attention_mask=full_mask[inds])
aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device) aux_loss = res.aux_loss if lm_config.use_moe else torch.tensor(0.0, device=args.device)
# 在 autocast 内计算 log_softmax,避免直接对 fp16/bf16 logits
# 计算造成额外数值偏差。
mb_resp_logp = F.log_softmax(res.logits[:, :-1], dim=-1).gather(2, labels[inds].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos[inds]) mb_resp_logp = F.log_softmax(res.logits[:, :-1], dim=-1).gather(2, labels[inds].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos[inds])
log_ratio = mb_resp_logp - old_resp_logp[inds] log_ratio = mb_resp_logp - old_resp_logp[inds]
@ -427,7 +423,6 @@ if __name__ == "__main__":
if args.use_compile == 1: if args.use_compile == 1:
actor_model = torch.compile(actor_model) actor_model = torch.compile(actor_model)
Logger('torch.compile enabled') Logger('torch.compile enabled')
rollout_engine.update_policy(actor_model)
if dist.is_initialized(): if dist.is_initialized():
# freqs_cos/freqs_sin 各 rank 由 config 确定性算出,默认每步广播一次纯属浪费 # freqs_cos/freqs_sin 各 rank 由 config 确定性算出,默认每步广播一次纯属浪费
actor_model = DistributedDataParallel(actor_model, device_ids=[local_rank], broadcast_buffers=False) actor_model = DistributedDataParallel(actor_model, device_ids=[local_rank], broadcast_buffers=False)

View File

@ -8,18 +8,39 @@ DATA_PATH = '../dataset/sft_t2t_mini.jsonl'
TOKENIZER_DIR = '../model_learn_tokenizer/' TOKENIZER_DIR = '../model_learn_tokenizer/'
VOCAB_SIZE = 6400 VOCAB_SIZE = 6400
SPECIAL_TOKENS_NUM = 36 SPECIAL_TOKENS_NUM = 36
MAX_LINES = 0 # 0 表示读取全部;设为正数则只取前 N 条(快速试跑)
def get_texts(data_path): def get_texts(data_path, max_lines=MAX_LINES):
"""逐行产出用于训练 BPE 的文本,兼容两种主线数据格式。
pretrain_t2t(_mini).jsonl 是 {"text": ...},
sft_t2t(_mini).jsonl 是 {"conversations": [{"role":..., "content":...}, ...]}。
原先只解析 conversations,所以把 DATA_PATH 指向预训练语料时一条文本都取不到,
BPE 仅基于 initial_alphabet 训练,产出一个没有任何 merge 的词表,且全程不报错。
"""
used = 0
with open(data_path, 'r', encoding='utf-8', errors='ignore') as f: with open(data_path, 'r', encoding='utf-8', errors='ignore') as f:
for i, line in enumerate(f): for line in f:
if i >= 10000: break # 选10000行测试 if max_lines and used >= max_lines:
break
try: try:
data = json.loads(line) data = json.loads(line)
contents = [item.get('content') for item in data.get('conversations', []) if item.get('content')]
if contents:
yield "\n".join(contents)
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
if 'text' in data:
text = str(data['text'])
else:
contents = [item.get('content') for item in data.get('conversations', []) if item.get('content')]
text = "\n".join(contents) if contents else ''
if text.strip():
used += 1
yield text
if used == 0:
raise ValueError(
f'{data_path} 中没有可用文本:每行应为 {{"text": ...}} 或 '
f'{{"conversations": [{{"role":..., "content":...}}, ...]}}。'
f'继续训练只会得到一个空词表。'
)
def train_tokenizer(data_path, tokenizer_dir, vocab_size, special_tokens_num=SPECIAL_TOKENS_NUM): def train_tokenizer(data_path, tokenizer_dir, vocab_size, special_tokens_num=SPECIAL_TOKENS_NUM):
tokenizer = Tokenizer(models.BPE()) tokenizer = Tokenizer(models.BPE())