Skip to content

Commit 744e958

Browse files
committed
feat: add nanochat_exp experiment module with training tools and documentation
- Add tokenizer training and data processing utilities - Add training monitoring and diagnosis tools - Add parquet inspection and viewing tools - Add documentation for data processing flow and known issues - Update dataloader, dataset, and training scripts
1 parent 3518fad commit 744e958

16 files changed

Lines changed: 4600 additions & 102 deletions
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# 数据处理流程说明
2+
3+
## 数据处理流程概览
4+
5+
整个数据处理流程分为以下几个阶段:
6+
7+
```
8+
原始数据 → 数据加载 → Parquet 转换 → Tokenization → 训练批次
9+
```
10+
11+
## 详细流程
12+
13+
### 1. 原始数据阶段(dataset.py)
14+
15+
**处理内容**:从 HuggingFace 数据集或本地 JSONL 文件加载原始数据
16+
17+
**数据格式**
18+
- HuggingFace 数据集:`{"text": "文档内容", ...}`
19+
- JSONL 文件:每行一个 JSON 对象,包含文本字段
20+
21+
**处理步骤**
22+
- 从 HuggingFace 下载或从本地加载数据集
23+
- 提取 `text` 字段(或自动识别文本字段)
24+
- 统一数据格式,只保留文本内容
25+
26+
**输出**:包含 `text` 列的 Dataset 对象
27+
28+
---
29+
30+
### 2. Parquet 转换阶段(dataset.py - `convert_to_parquet()`
31+
32+
**处理内容**:将文本数据转换为 Parquet 格式,便于高效读取
33+
34+
**数据格式**
35+
- 输入:文本列表 `["文档1", "文档2", ...]`
36+
- 输出:Parquet 文件,每行一个文档的文本内容
37+
38+
**处理步骤**
39+
- 将文本按字符数分片(默认每片 ~250M 字符)
40+
- 写入 Parquet 文件(使用 Snappy 压缩)
41+
- 文件命名:`shard_00000.parquet`, `shard_00001.parquet`, ...
42+
43+
**输出**:Parquet 文件目录 `parquet_shards/`
44+
45+
**关键代码位置**
46+
```python
47+
# dataset.py:358-457
48+
def convert_to_parquet(...):
49+
# 提取 text 字段
50+
text = example.get(text_column, "")
51+
# 写入 Parquet
52+
table = pa.Table.from_arrays([pa.array(texts)], names=["text"])
53+
pq.write_table(table, filepath, compression="snappy")
54+
```
55+
56+
---
57+
58+
### 3. 数据加载阶段(dataloader.py - `parquets_iter_batched()`
59+
60+
**处理内容**:从 Parquet 文件读取文本数据
61+
62+
**数据格式**
63+
- 输入:Parquet 文件
64+
- 输出:文本批次 `["文档1", "文档2", ..., "文档128"]`
65+
66+
**处理步骤**
67+
- 按 DDP rank 分片读取(分布式训练)
68+
- 从 Parquet 的 `text` 列读取文本
69+
-`tokenizer_batch_size`(默认 128)分批返回
70+
71+
**输出**:文本批次迭代器
72+
73+
**关键代码位置**
74+
```python
75+
# dataset.py:475-504
76+
def parquets_iter_batched(...):
77+
rg = pf.read_row_group(rg_idx)
78+
texts = rg.column('text').to_pylist() # 读取 text 列
79+
yield texts
80+
```
81+
82+
---
83+
84+
### 4. Tokenization 阶段(dataloader.py + tokenizer.py)⭐
85+
86+
**处理内容**:将文本转换为 token IDs(这是 tokenizer 处理的部分)
87+
88+
**数据格式**
89+
- 输入:文本批次 `["文档1", "文档2", ...]`
90+
- 输出:Token ID 列表 `[[1, 234, 567, ...], [1, 890, 123, ...], ...]`
91+
92+
**处理步骤**
93+
1. **获取 tokenizer**`tokenizer = get_tokenizer()`
94+
2. **添加 BOS token**:在每个文档前添加 `<|bos|>` token
95+
3. **编码文本**`tokenizer.encode(doc_batch, prepend=bos_token)`
96+
4. **返回 token IDs**:每个文档转换为一个 token ID 列表
97+
98+
**关键代码位置**
99+
```python
100+
# dataloader.py:108-114
101+
token_lists = tokenizer.encode(
102+
doc_batch, # 文本批次:["文档1", "文档2", ...]
103+
prepend=bos_token, # 在每个文档前添加 BOS token
104+
num_threads=tokenizer_threads
105+
)
106+
# 返回:[[1, 234, 567, ...], [1, 890, 123, ...], ...]
107+
```
108+
109+
**Tokenizer 处理的具体内容**
110+
-**文本字符串****Token IDs**
111+
- ✅ 使用 BPE(Byte Pair Encoding)算法
112+
- ✅ 添加特殊 token(BOS、EOS 等)
113+
- ✅ 处理 Unicode 字符和多语言文本
114+
115+
---
116+
117+
### 5. 批次构建阶段(dataloader.py)
118+
119+
**处理内容**:将 token IDs 组织成训练批次
120+
121+
**数据格式**
122+
- 输入:Token ID 流 `[1, 234, 567, 890, ...]`
123+
- 输出:训练批次 `(inputs, targets)` tensors
124+
125+
**处理步骤**
126+
1. **累积 tokens**:从 token buffer 中取出 `B * T + 1` 个 tokens
127+
2. **创建 inputs/targets**
128+
- `inputs = tokens[:-1]` (前 B*T 个 tokens)
129+
- `targets = tokens[1:]` (后 B*T 个 tokens,用于预测)
130+
3. **重塑形状**`(B, T)` - B 个样本,每个 T 个 tokens
131+
4. **移动到设备**:CPU → GPU(如果使用 CUDA)
132+
133+
**关键代码位置**
134+
```python
135+
# dataloader.py:117-143
136+
tokens = [token_buffer.popleft() for _ in range(needed_tokens)] # B*T+1 个 tokens
137+
inputs_cpu = scratch[:-1].to(dtype=torch.int32) # 前 B*T 个
138+
targets_cpu = scratch[1:] # 后 B*T 个
139+
inputs = inputs_cpu.view(B, T).to(device=device) # 重塑为 (B, T)
140+
targets = targets_cpu.view(B, T).to(device=device)
141+
```
142+
143+
---
144+
145+
## Tokenizer 处理的数据部分总结
146+
147+
### ✅ Tokenizer 处理的内容:
148+
149+
1. **文本字符串****Token IDs**
150+
- 输入:纯文本字符串(如 "Hello world")
151+
- 输出:整数列表(如 `[15496, 1917]`
152+
153+
2. **处理位置**
154+
-`dataloader.py``tokenizing_distributed_data_loader()` 函数中
155+
- 调用 `tokenizer.encode()` 方法
156+
157+
3. **处理时机**
158+
- 从 Parquet 文件读取文本后
159+
- 在构建训练批次之前
160+
161+
4. **处理方式**
162+
- 批量处理(默认 128 个文档一批)
163+
- 每个文档前添加 BOS token
164+
- 使用多线程加速(默认 4 个线程)
165+
166+
### ❌ Tokenizer 不处理的内容:
167+
168+
- ❌ 数据下载和加载(dataset.py)
169+
- ❌ Parquet 文件转换(dataset.py)
170+
- ❌ 批次构建和 tensor 操作(dataloader.py)
171+
- ❌ GPU 内存管理(dataloader.py)
172+
173+
---
174+
175+
## 数据流示例
176+
177+
```
178+
原始数据:
179+
{"text": "这是一个测试文档。"}
180+
181+
↓ (dataset.py - convert_to_parquet)
182+
183+
Parquet 文件:
184+
text: "这是一个测试文档。"
185+
186+
↓ (dataloader.py - parquets_iter_batched)
187+
188+
文本批次:
189+
["这是一个测试文档。"]
190+
191+
↓ (dataloader.py + tokenizer.py - encode) ⭐ TOKENIZER 处理这里
192+
193+
Token IDs:
194+
[[1, 234, 567, 890, 1234, 5678]] # 1 是 BOS token
195+
196+
↓ (dataloader.py - 批次构建)
197+
198+
训练批次:
199+
inputs: [[1, 234, 567, 890, 1234]] # shape: (B, T)
200+
targets: [[234, 567, 890, 1234, 5678]] # shape: (B, T)
201+
```
202+
203+
---
204+
205+
## 关键文件说明
206+
207+
| 文件 | 职责 | 处理的数据部分 |
208+
|------|------|---------------|
209+
| `dataset.py` | 数据加载和转换 | 原始数据 → Parquet 文件 |
210+
| `dataloader.py` | 数据加载和批次构建 | Parquet 文件 → 训练批次 |
211+
| `tokenizer.py` | Tokenization | **文本字符串 → Token IDs**|
212+
213+
---
214+
215+
## 总结
216+
217+
**Tokenizer 处理的是数据的 Tokenization 阶段**,具体来说:
218+
219+
1. **输入**:从 Parquet 文件读取的**文本字符串**
220+
2. **处理**:使用 BPE 算法将文本转换为 **Token IDs**
221+
3. **输出****Token ID 列表**,供后续批次构建使用
222+
223+
这是整个数据处理流程中的**关键步骤**,将人类可读的文本转换为模型可以处理的数字序列。

examples/nanochat_exp/README.md

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This directory provides integration between OpenSeek datasets and the [nanochat]
44

55
## Overview
66

7-
This module adapts OpenSeek datasets (specifically OpenSeek-Pretrain-100B) to work with nanochat's training pipeline. It provides:
7+
This module adapts OpenSeek datasets (specifically OpenSeek-Pretrain-Data-Examples) to work with nanochat's training pipeline. It provides:
88

99
- **Dataset conversion**: Converts OpenSeek datasets from HuggingFace format to parquet format compatible with nanochat
1010
- **Data loader**: Provides a data loader compatible with nanochat's training interface
@@ -23,12 +23,25 @@ This module adapts OpenSeek datasets (specifically OpenSeek-Pretrain-100B) to wo
2323

2424
2. **Python dependencies**(推荐 Python≥3.10、PyTorch≥2.1,与 nanochat 官方示例保持一致):
2525
```bash
26-
pip install pyarrow datasets huggingface_hub
26+
# 推荐使用虚拟环境(避免权限警告)
27+
python -m venv venv
28+
source venv/bin/activate # Linux/Mac
29+
# 或 venv\Scripts\activate # Windows
30+
31+
# OpenSeek 使用 HuggingFace tokenizers 库(易于安装,无需 Rust 编译)
32+
pip install pyarrow datasets huggingface_hub tokenizers>=0.22.0
2733
```
28-
29-
3. **OpenSeek dataset**: Download the OpenSeek-Pretrain-100B dataset:
34+
35+
> **重要**: OpenSeek 使用 HuggingFace `tokenizers` 库替代 nanochat 的 `rustbpe` 模块。
36+
> - **优势**: `tokenizers` 库更容易安装,只需 `pip install tokenizers`,无需 Rust 编译
37+
> - **兼容性**: 完全兼容 nanochat 的 tokenizer 接口
38+
> - **性能**: HuggingFace tokenizers 库性能优秀,基于 Rust 实现但提供预编译的 Python 包
39+
>
40+
> 如果以 root 用户运行 pip,会收到警告。建议使用虚拟环境,或使用 `--root-user-action=ignore` 选项(仅在明确知道自己在做什么时使用)。
41+
42+
3. **OpenSeek dataset**: Download the OpenSeek-Pretrain-Data-Examples dataset:
3043
- Option 1: Download from HuggingFace (automatic)
31-
- Option 2: Download manually to `OpenSeek-Pretrain-100B/` directory in the project root
44+
- Option 2: Download manually to `OpenSeek-Pretrain-Data-Examples/` directory in the project root
3245

3346
## Quick Start
3447

@@ -49,37 +62,62 @@ First, convert the OpenSeek dataset to the parquet format expected by nanochat:
4962

5063
```bash
5164
# From OpenSeek root directory
65+
# 对于示例数据集,默认使用 -1(处理所有数据,不限制 shards 数量)
5266
python -m examples.nanochat_exp.dataset \
53-
--dataset "BAAI/OpenSeek-Pretrain-100B" \
54-
--num-shards 240 \
67+
--dataset "BAAI/OpenSeek-Pretrain-Data-Examples" \
68+
--num-shards -1 \
5569
--streaming
5670
```
5771

5872
Or if you have the dataset locally:
5973

6074
```bash
6175
python -m examples.nanochat_exp.dataset \
62-
--dataset ./OpenSeek-Pretrain-100B \
63-
--num-shards 240
76+
--dataset ./OpenSeek-Pretrain-Data-Examples \
77+
--num-shards -1
6478
```
6579

6680
This will create parquet shards in `~/.cache/openseek_nanochat/parquet_shards/` (or the directory specified by `OPENSEEK_NANOCHAT_DATA_DIR`).
6781

68-
### 2. Modify nanochat Training Scripts
82+
### 2. Train Tokenizer (Optional but Recommended)
6983

70-
To use OpenSeek data with nanochat, you need to modify nanochat's training scripts to use our data loader. In `nanochat/scripts/base_train.py`, change:
84+
Train a BPE tokenizer from your data using HuggingFace tokenizers (no rustbpe needed):
7185

72-
```python
73-
from nanochat.dataloader import tokenizing_distributed_data_loader
86+
```bash
87+
# From OpenSeek root directory
88+
python -m examples.nanochat_exp.tok_train \
89+
--vocab-size 50257 \
90+
--data-dir ~/.cache/openseek_nanochat/parquet_shards
91+
```
92+
93+
This will create `tokenizer.json` in the tokenizer directory. The script uses HuggingFace tokenizers library, which is much easier to install than rustbpe (no Rust compilation needed).
94+
95+
### 3. Modify nanochat Training Scripts
96+
97+
To use OpenSeek data with nanochat, you need to modify nanochat's training scripts to use our data loader and tokenizer. You can use the automated patch script:
98+
99+
```bash
100+
# From OpenSeek root directory
101+
python -m examples.nanochat_exp.patch_nanochat --nanochat-path /path/to/nanochat
74102
```
75103

76-
to:
104+
This will automatically modify `nanochat/scripts/base_train.py` to:
105+
- Use OpenSeek's dataloader (from `examples.nanochat_exp.dataloader`)
106+
- Use OpenSeek's tokenizer (from `examples.nanochat_exp.tokenizer`, uses HuggingFace tokenizers, no rustbpe)
107+
108+
Alternatively, you can manually modify `nanochat/scripts/base_train.py`:
77109

78110
```python
111+
# Change this:
112+
from nanochat.dataloader import tokenizing_distributed_data_loader
113+
from nanochat.tokenizer import get_tokenizer
114+
115+
# To this:
79116
from examples.nanochat_exp.dataloader import tokenizing_distributed_data_loader
117+
from examples.nanochat_exp.tokenizer import get_tokenizer
80118
```
81119

82-
### 3. Run Training
120+
### 4. Run Training
83121

84122
After modifying the import, you can run nanochat training as usual:
85123

@@ -100,6 +138,9 @@ examples/nanochat_exp/
100138
├── __init__.py # Module initialization
101139
├── dataset.py # Dataset conversion and loading utilities
102140
├── dataloader.py # Data loader compatible with nanochat
141+
├── tokenizer.py # Tokenizer wrapper using HuggingFace tokenizers (easy install)
142+
├── tok_train.py # Tokenizer training script (uses HuggingFace tokenizers, no rustbpe)
143+
├── patch_nanochat.py # Script to patch nanochat's base_train.py (replaces dataloader & tokenizer)
103144
├── run_openseek_exp.sh # Experiment runner script
104145
├── train_wrapper.py # Training wrapper script
105146
└── README.md # This file
@@ -109,10 +150,37 @@ This module can be imported via `examples.nanochat_exp` when the repository root
109150

110151
## Design Notes & Configuration
111152

153+
### Tokenizer 说明
154+
155+
OpenSeek 使用 **HuggingFace tokenizers 库**替代 nanochat 的 `rustbpe` 模块:
156+
157+
- **优势**:
158+
- 更容易安装:只需 `pip install tokenizers`,无需 Rust 编译
159+
- 完全兼容:提供与 nanochat tokenizer 相同的接口
160+
- 性能优秀:基于 Rust 实现,提供预编译的 Python 包
161+
162+
- **使用方式**:
163+
- `examples.nanochat_exp.tokenizer.get_tokenizer()` 会自动使用 HuggingFace tokenizers
164+
- 如果找不到 tokenizer.json,会尝试从标准位置加载,或创建默认 tokenizer
165+
- 数据加载器会自动使用新的 tokenizer,无需修改代码
166+
- 训练 tokenizer 使用 `python -m examples.nanochat_exp.tok_train`(无需 rustbpe)
167+
168+
- **训练 Tokenizer**:
169+
- 使用 `examples.nanochat_exp.tok_train` 脚本训练 BPE tokenizer
170+
- 完全替代 nanochat 的 `tok_train.py`,无需 rustbpe
171+
- 支持自定义词汇表大小、最小频率等参数
172+
- 输出标准的 `tokenizer.json` 文件,兼容 HuggingFace tokenizers
173+
174+
- **兼容性**:
175+
- 如果系统中已安装 rustbpe,代码会优先尝试使用 HuggingFace tokenizers
176+
- 如果 HuggingFace tokenizers 不可用,会回退到 nanochat 的 rustbpe(如果可用)
177+
- 训练脚本完全独立,不依赖 rustbpe
178+
112179
### 数据加载接口说明
113180

114181
- `examples.nanochat_exp.dataloader.tokenizing_distributed_data_loader()` 保持与 nanochat 原生接口兼容,可直接替换 import。
115-
- 默认 tokenizer 仍由 nanochat 配置决定;若需变更 tokenizer、最大长度或 padding 规则,可在 `train_wrapper.py` 中调整对应函数,再由训练脚本调用。
182+
- Tokenizer 使用 HuggingFace tokenizers 库(易于安装),完全兼容 nanochat 的 tokenizer 接口。
183+
- 若需变更 tokenizer、最大长度或 padding 规则,可在 `train_wrapper.py` 中调整对应函数,再由训练脚本调用。
116184
- 批量大小、梯度累积等策略建议继续放在 nanochat 脚本侧统一配置,确保与数据加载逻辑一致。
117185

118186
### Environment Variables

0 commit comments

Comments
 (0)