Compare commits
14 Commits
65fa8d0701
...
5118b8c96e
| Author | SHA1 | Date |
|---|---|---|
|
|
5118b8c96e | |
|
|
7f5ba40a4e | |
|
|
949488fdb4 | |
|
|
e41bf7cacf | |
|
|
b330a4d5c0 | |
|
|
b956bad49f | |
|
|
d698c06fc5 | |
|
|
984a2c3611 | |
|
|
d4b67f1783 | |
|
|
ba10c4d17b | |
|
|
cb08b9300b | |
|
|
56d4cddfd8 | |
|
|
0c91dea4a2 | |
|
|
743420daac |
|
|
@ -4,3 +4,9 @@ __pycache__/
|
||||||
out
|
out
|
||||||
website/
|
website/
|
||||||
docs-minimind/
|
docs-minimind/
|
||||||
|
|
||||||
|
checkpoints/
|
||||||
|
dataset/
|
||||||
|
|
||||||
|
minimind-3/
|
||||||
|
minimind-3-moe/
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,12 @@ 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:
|
||||||
|
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', '')
|
prompt_content = prompt_content.replace('<think>\n\n</think>\n\n', '')
|
||||||
return prompt_content
|
return prompt_content
|
||||||
|
|
||||||
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# .gitignore 忽略下列文件和目录
|
||||||
|
|
||||||
|
#比如minimind\scripts\minimind-3和minimind\scripts\minimind-3-moe
|
||||||
|
minimind-3/
|
||||||
|
minimind-3-moe/
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,28 @@ st.markdown("""
|
||||||
margin: 0 2px !important; /* 调整这里的 margin 值 */
|
margin: 0 2px !important; /* 调整这里的 margin 值 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 单独恢复侧边栏按钮(新建对话)的正常横向长方形外观,避免文字被挤成竖排 */
|
||||||
|
[data-testid="stSidebar"] .stButton > button {
|
||||||
|
border-radius: 8px !important; /* 恢复稍微圆角的矩形 */
|
||||||
|
width: 100% !important; /* 撑满侧边栏宽度 */
|
||||||
|
height: auto !important;
|
||||||
|
min-width: 100% !important; /* 覆盖掉 18px 的限制 */
|
||||||
|
min-height: 40px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: none !important;
|
||||||
|
padding: 8px !important; /* 恢复内边距 */
|
||||||
|
background-color: #f4f6f8 !important; /* 增加一点按钮底色以免透明看不清 */
|
||||||
|
border: 1px solid #e0e0e0 !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
color: #333 !important;
|
||||||
|
margin: 10px 0 !important;
|
||||||
|
}
|
||||||
|
[data-testid="stSidebar"] .stButton > button:hover {
|
||||||
|
border-color: #999 !important;
|
||||||
|
background-color: #e4e8f0 !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|
@ -87,6 +109,7 @@ LANG_TEXTS = {
|
||||||
'disclaimer': 'AI 生成内容可能存在错误,请仔细核实',
|
'disclaimer': 'AI 生成内容可能存在错误,请仔细核实',
|
||||||
'think_tip': '自适应思考,目前多轮对话或Tool Call共存时思考不稳定',
|
'think_tip': '自适应思考,目前多轮对话或Tool Call共存时思考不稳定',
|
||||||
'tool_select': '工具选择(最多4个)',
|
'tool_select': '工具选择(最多4个)',
|
||||||
|
'new_chat': '新建对话', # 新增
|
||||||
},
|
},
|
||||||
'en': {
|
'en': {
|
||||||
'settings': 'Model Settings',
|
'settings': 'Model Settings',
|
||||||
|
|
@ -100,6 +123,7 @@ LANG_TEXTS = {
|
||||||
'disclaimer': 'AI-generated content may be inaccurate, please verify',
|
'disclaimer': 'AI-generated content may be inaccurate, please verify',
|
||||||
'think_tip': 'Adaptive thinking; may be unstable with multi-turn or Tool Call',
|
'think_tip': 'Adaptive thinking; may be unstable with multi-turn or Tool Call',
|
||||||
'tool_select': 'Tool Selection (max 4)',
|
'tool_select': 'Tool Selection (max 4)',
|
||||||
|
'new_chat': 'New Chat', # 新增
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,15 +265,20 @@ def regenerate_answer(index):
|
||||||
|
|
||||||
|
|
||||||
# 动态扫描模型目录
|
# 动态扫描模型目录
|
||||||
|
@st.cache_data(ttl=3600) # 增加缓存(有效期1小时),避免每次刷新页面都去高频读取硬盘文件树
|
||||||
|
def get_model_paths():
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
MODEL_PATHS = {}
|
paths = {}
|
||||||
for d in sorted(os.listdir(script_dir), reverse=True):
|
for d in sorted(os.listdir(script_dir), reverse=True):
|
||||||
full_path = os.path.join(script_dir, d)
|
full_path = os.path.join(script_dir, d)
|
||||||
if os.path.isdir(full_path) and not d.startswith('.') and not d.startswith('_'):
|
if os.path.isdir(full_path) and not d.startswith('.') and not d.startswith('_'):
|
||||||
if any(f.endswith(('.bin', '.safetensors', '.pt')) or os.path.exists(os.path.join(full_path, 'model.safetensors.index.json')) for f in os.listdir(full_path) if os.path.isfile(os.path.join(full_path, f))):
|
if any(f.endswith(('.bin', '.safetensors', '.pt')) or os.path.exists(os.path.join(full_path, 'model.safetensors.index.json')) for f in os.listdir(full_path) if os.path.isfile(os.path.join(full_path, f))):
|
||||||
MODEL_PATHS[d] = [d, d]
|
paths[d] =[d, d]
|
||||||
if not MODEL_PATHS:
|
if not paths:
|
||||||
MODEL_PATHS = {"No models found": ["", "No models"]}
|
paths = {"No models found": ["", "No models"]}
|
||||||
|
return paths
|
||||||
|
|
||||||
|
MODEL_PATHS = get_model_paths()
|
||||||
|
|
||||||
# 模型选择
|
# 模型选择
|
||||||
selected_model = st.sidebar.selectbox('Model', list(MODEL_PATHS.keys()), index=0)
|
selected_model = st.sidebar.selectbox('Model', list(MODEL_PATHS.keys()), index=0)
|
||||||
|
|
@ -289,7 +318,27 @@ with st.sidebar.expander(get_text('tools')):
|
||||||
if checked and len(st.session_state.selected_tools) < 4:
|
if checked and len(st.session_state.selected_tools) < 4:
|
||||||
st.session_state.selected_tools.append(name)
|
st.session_state.selected_tools.append(name)
|
||||||
|
|
||||||
image_url = "https://www.modelscope.cn/api/v1/studio/gongjy/MiniMind/repo?Revision=master&FilePath=images%2Flogo2.png&View=true"
|
# 新建对话
|
||||||
|
st.sidebar.markdown('<hr style="margin: 12px 0 16px 0;">', unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# 新建对话
|
||||||
|
if st.sidebar.button(get_text('new_chat'), key="new_chat", use_container_width=True):
|
||||||
|
clear_chat_messages()
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# 将外部网络图片改为读取本地图片转 base64 渲染
|
||||||
|
import base64
|
||||||
|
@st.cache_data
|
||||||
|
def get_image_base64(image_path):
|
||||||
|
try:
|
||||||
|
with open(image_path, "rb") as img_file:
|
||||||
|
return f"data:image/png;base64,{base64.b64encode(img_file.read()).decode()}"
|
||||||
|
except:
|
||||||
|
# 如果本地图片不存在,再退回使用网络URL
|
||||||
|
return "https://www.modelscope.cn/api/v1/studio/gongjy/MiniMind/repo?Revision=master&FilePath=images%2Flogo2.png&View=true"
|
||||||
|
|
||||||
|
local_logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "images", "logo2.png")
|
||||||
|
image_url = get_image_base64(local_logo_path)
|
||||||
|
|
||||||
st.markdown(
|
st.markdown(
|
||||||
f'<div style="display: flex; flex-direction: column; align-items: center; text-align: center; margin: 0; padding: 0;">'
|
f'<div style="display: flex; flex-direction: column; align-items: center; text-align: center; margin: 0; padding: 0;">'
|
||||||
|
|
|
||||||
|
|
@ -471,7 +471,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)
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,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)
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,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)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue