Compare commits

...

13 Commits

Author SHA1 Message Date
Dujus 1095e2efdc
Merge ccb352f901 into 949488fdb4 2026-09-20 20:12:17 +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
Daoyuan Li 56d4cddfd8 [fix] keep DPO preference pair think formatting aligned 2026-08-29 18:29:00 -07:00
Dujus ccb352f901 docs: add windows tips in README_EN.md 2026-06-01 11:13:08 +08:00
Dujus 99c26e9043 docs: optimize installation steps for Windows beginners and add HF mirror tip 2026-06-01 10:59:18 +08:00
7 changed files with 61 additions and 15 deletions

View File

@ -230,6 +230,10 @@ minimind2系列旧模型均经过权重映射+(微调训练)QKVO线性层校
# 克隆仓库、安装依赖 # 克隆仓库、安装依赖
git clone --depth 1 https://github.com/jingyaogong/minimind git clone --depth 1 https://github.com/jingyaogong/minimind
cd minimind && pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple cd minimind && pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
#部分老版本Windows powershell不能识别&&,Windows用户应将&&链式命令改为分行写法
git clone --depth 1 https://github.com/jingyaogong/minimind
cd minimind
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
``` ```
## Ⅰ 🚀 模型推理 ## Ⅰ 🚀 模型推理
@ -242,6 +246,13 @@ cd minimind && pip install -r requirements.txt -i https://mirrors.aliyun.com/pyp
modelscope download --model gongjy/minimind-3 --local_dir ./minimind-3 modelscope download --model gongjy/minimind-3 --local_dir ./minimind-3
# 方式2 # 方式2
git clone https://huggingface.co/jingyaogong/minimind-3 git clone https://huggingface.co/jingyaogong/minimind-3
# 国内网络进入huggingface网站存在困难,可使用镜像站
# Windows
$env:HF_ENDPOINT = "https://hf-mirror.com"
git clone https://hf-mirror.com/jingyaogong/minimind-3
#Linux
export HF_ENDPOINT="https://hf-mirror.com"
git clone https://hf-mirror.com/jingyaogong/minimind-3
``` ```
### 2' CLI 推理 ### 2' CLI 推理

View File

@ -229,6 +229,10 @@ After this update, maintenance for the entire `minimind-v1` series will be disco
# Clone repository and install dependencies # Clone repository and install dependencies
git clone --depth 1 https://github.com/jingyaogong/minimind git clone --depth 1 https://github.com/jingyaogong/minimind
cd minimind && pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple cd minimind && pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
#Tip:Some older versions of Windows PowerShell (v5.1 and below) do not recognize the && operator.
git clone --depth 1 https://github.com/jingyaogong/minimind
cd minimind
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
``` ```
## Ⅰ 🚀 Model Inference ## Ⅰ 🚀 Model Inference

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,14 @@ 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) # 先转 float32 再加 mask:-1e9 超出 float16 的表示范围(min≈-65504),
# 在 float16 张量上累加会饱和成 -inf。若某个 query 在因果范围内可见的
# key 全被 mask 掉(左 padding 时必然出现),softmax 整行就是 NaN,
# 并在下一层通过 K/V 投影扩散到同序列里有效的 token 上。
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

@ -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())