Compare commits
6 Commits
34fc0b12fe
...
8848692437
| Author | SHA1 | Date |
|---|---|---|
|
|
8848692437 | |
|
|
89d674b8a5 | |
|
|
2d4c385b0f | |
|
|
e9f46506da | |
|
|
1191fc8e9f | |
|
|
512eed0b65 |
|
|
@ -765,16 +765,16 @@ LoRA 是一种常见的参数高效微调(Parameter-Efficient Fine-Tuning, PEF
|
|||
```bash
|
||||
# train_lora.py 在 CPU 上通常也能比较轻快地完成
|
||||
# 方式1
|
||||
torchrun --nproc_per_node 1 train_lora.py
|
||||
cd trainer && torchrun --nproc_per_node 1 train_lora.py
|
||||
# 方式2
|
||||
python train_lora.py
|
||||
cd trainer && python train_lora.py
|
||||
```
|
||||
|
||||
> 训练后的模型权重文件默认每隔`save_interval步`保存为: `lora_xxx_*.pth`(*为模型具体dimension,每次保存时新文件会覆盖旧文件)
|
||||
|
||||
|
||||
LoRA 很适合处理“如何在尽量保留通用能力的前提下,让模型快速适应私有领域或垂直场景”这类问题。例如基础模型医学知识不足时,就可以在原有模型之上叠加一层面向医疗场景的 LoRA 权重,以较小代价获得更好的领域表现。
|
||||
通常只需要准备同样的多轮对话格式数据,放置到 `lora_xxx.jsonl`,再执行 `python train_lora.py`,即可得到新的 `LoRA` 模型权重。
|
||||
通常只需要准备同样的多轮对话格式数据,放置到 `lora_xxx.jsonl`,再从仓库根目录执行 `cd trainer && python train_lora.py`,即可得到新的 `LoRA` 模型权重。
|
||||
|
||||
例1:垂域数据
|
||||
|
||||
|
|
|
|||
|
|
@ -765,16 +765,16 @@ Its core idea is to introduce low-rank incremental branches alongside the origin
|
|||
```bash
|
||||
# train_lora.py can usually be completed fairly quickly even on CPU
|
||||
# Method 1
|
||||
torchrun --nproc_per_node 1 train_lora.py
|
||||
cd trainer && torchrun --nproc_per_node 1 train_lora.py
|
||||
# Method 2
|
||||
python train_lora.py
|
||||
cd trainer && python train_lora.py
|
||||
```
|
||||
|
||||
> The trained model weight files are saved by default every `save_interval steps` as: `lora_xxx_*.pth` (* is the specific model dimension, each save overwrites the previous file)
|
||||
|
||||
|
||||
LoRA is well-suited for handling problems like "how to let the model quickly adapt to private domains or vertical scenarios while preserving general capabilities as much as possible." For example, when the base model lacks medical knowledge, a medical-oriented LoRA weight layer can be stacked on top of the original model to achieve better domain performance at relatively small cost.
|
||||
Usually you only need to prepare multi-turn dialogue format data in the same way, place it in `lora_xxx.jsonl`, and then run `python train_lora.py` to obtain new `LoRA` model weights.
|
||||
Usually you only need to prepare multi-turn dialogue format data in the same way, place it in `lora_xxx.jsonl`, and then run `cd trainer && python train_lora.py` from the repository root to obtain new `LoRA` model weights.
|
||||
|
||||
Example 1: Vertical domain data
|
||||
|
||||
|
|
|
|||
|
|
@ -21,13 +21,19 @@ while True:
|
|||
extra_body={"chat_template_kwargs": {"open_thinking": True}, "reasoning_effort": "medium"} # 思考开关
|
||||
)
|
||||
if not stream:
|
||||
if not response.choices or response.choices[0].message is None:
|
||||
raise ValueError("LLM returned empty or filtered response")
|
||||
assistant_res = response.choices[0].message.content
|
||||
print('[A]: ', assistant_res)
|
||||
else:
|
||||
print('[A]: ', end='', flush=True)
|
||||
assistant_res = ''
|
||||
for chunk in response:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
delta = chunk.choices[0].delta
|
||||
if delta is None:
|
||||
continue
|
||||
r = getattr(delta, 'reasoning_content', None) or ""
|
||||
c = delta.content or ""
|
||||
if r:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from threading import Thread
|
|||
from queue import Queue
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
|
||||
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
|
||||
from model.model_lora import apply_lora, load_lora
|
||||
|
|
@ -54,7 +54,7 @@ class ChatRequest(BaseModel):
|
|||
top_p: float = 0.92
|
||||
max_tokens: int = 8192
|
||||
stream: bool = True
|
||||
tools: list = []
|
||||
tools: list = Field(default_factory=list)
|
||||
open_thinking: bool = False
|
||||
chat_template_kwargs: dict = None
|
||||
|
||||
|
|
@ -104,24 +104,28 @@ def parse_response(text):
|
|||
|
||||
def generate_stream_response(messages, temperature, top_p, max_tokens, tools=None, open_thinking=False):
|
||||
try:
|
||||
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)[-max_tokens:]
|
||||
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)
|
||||
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
||||
|
||||
queue = Queue()
|
||||
streamer = CustomStreamer(tokenizer, queue)
|
||||
|
||||
def _generate():
|
||||
model.generate(
|
||||
inputs.input_ids,
|
||||
max_new_tokens=max_tokens,
|
||||
do_sample=True,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
attention_mask=inputs.attention_mask,
|
||||
pad_token_id=tokenizer.pad_token_id,
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
streamer=streamer
|
||||
)
|
||||
try:
|
||||
model.generate(
|
||||
inputs.input_ids,
|
||||
max_new_tokens=max_tokens,
|
||||
do_sample=True,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
attention_mask=inputs.attention_mask,
|
||||
pad_token_id=tokenizer.pad_token_id,
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
streamer=streamer
|
||||
)
|
||||
except Exception as e:
|
||||
queue.put({"error": str(e)})
|
||||
queue.put(None)
|
||||
|
||||
Thread(target=_generate).start()
|
||||
|
||||
|
|
@ -133,6 +137,9 @@ def generate_stream_response(messages, temperature, top_p, max_tokens, tools=Non
|
|||
text = queue.get()
|
||||
if text is None:
|
||||
break
|
||||
if isinstance(text, dict):
|
||||
yield json.dumps(text, ensure_ascii=False)
|
||||
continue
|
||||
full_text += text
|
||||
|
||||
if not thinking_ended:
|
||||
|
|
@ -190,7 +197,7 @@ async def chat_completions(request: ChatRequest):
|
|||
add_generation_prompt=True,
|
||||
tools=request.tools or None,
|
||||
open_thinking=request.get_open_thinking()
|
||||
)[-request.max_tokens:]
|
||||
)
|
||||
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
||||
with torch.no_grad():
|
||||
generated_ids = model.generate(
|
||||
|
|
|
|||
|
|
@ -339,8 +339,8 @@ def main():
|
|||
st.markdown(
|
||||
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: #3d4450; border-radius: 22px; color: white;">{prompt}</div></div>',
|
||||
unsafe_allow_html=True)
|
||||
messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})
|
||||
st.session_state.chat_messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
st.session_state.chat_messages.append({"role": "user", "content": prompt})
|
||||
|
||||
placeholder = st.empty()
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import warnings
|
|||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn.functional as F
|
||||
from transformers import AutoTokenizer
|
||||
from contextlib import nullcontext
|
||||
from torch import optim, nn
|
||||
from torch.nn.parallel import DistributedDataParallel
|
||||
|
|
@ -76,7 +75,7 @@ def calculate_rewards(prompts, responses, reward_model):
|
|||
return rewards
|
||||
|
||||
|
||||
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):
|
||||
def ppo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, actor_scheduler, critic_scheduler, reward_model, start_step=0, wandb=None):
|
||||
actor_model.train()
|
||||
critic_model.train()
|
||||
grad_accum_step = 0
|
||||
|
|
@ -174,10 +173,24 @@ def ppo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, actor_sched
|
|||
with autocast_ctx:
|
||||
res = actor_unwrapped(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)
|
||||
|
||||
mb_resp_logp = F.log_softmax(res.logits[:, :-1], dim=-1).gather(2, labels[inds].unsqueeze(-1)).squeeze(-1).gather(1, logp_pos[inds])
|
||||
# 在 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])
|
||||
|
||||
log_ratio = mb_resp_logp - old_resp_logp[inds]
|
||||
|
||||
# 可开关的诊断:观察首轮首个 minibatch 的 mb 与 old logp 差异。
|
||||
if args.debug_log_ratio and ppo_epoch == 0 and i == 0 and is_main_process():
|
||||
_lr = log_ratio.detach()
|
||||
_m = resp_policy_mask[inds].bool()
|
||||
if _m.any():
|
||||
_lrv = _lr[_m]
|
||||
Logger(f"[DBG log_ratio] step={step} max|lr|={_lrv.abs().max().item():.6e} "
|
||||
f"mean|lr|={_lrv.abs().mean().item():.6e} "
|
||||
f"ratio_max={torch.exp(_lrv).max().item():.6f} "
|
||||
f"ratio_min={torch.exp(_lrv).min().item():.6f} "
|
||||
f"dropout={getattr(lm_config, 'dropout', None)} "
|
||||
f"training={actor_unwrapped.training}")
|
||||
approx_kl = (0.5 * (log_ratio ** 2) * resp_policy_mask[inds]).sum() / resp_policy_mask[inds].sum().clamp(min=1)
|
||||
|
||||
# 同步各卡的 approx_kl,防止某卡 break 而其它卡继续导致 DDP 死锁
|
||||
|
|
@ -331,6 +344,7 @@ if __name__ == "__main__":
|
|||
parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")
|
||||
parser.add_argument("--debug_mode", action="store_true", help="是否打印训练调试采样")
|
||||
parser.add_argument("--debug_interval", type=int, default=20, help="debug模式下每隔多少step打印一次采样")
|
||||
parser.add_argument("--debug_log_ratio", action="store_true", help="打印首轮首个minibatch的log_ratio差异量级,用于核查ratio≈1是否成立")
|
||||
parser.add_argument("--thinking_ratio", type=float, default=0.9, help="按概率开启thinking(0.0~1.0)")
|
||||
parser.add_argument("--rollout_engine", type=str, default="torch", choices=["torch", "sglang"], help="rollout引擎类型")
|
||||
parser.add_argument("--sglang_base_url", type=str, default="http://localhost:8998", help="SGLang服务器URL")
|
||||
|
|
@ -427,9 +441,9 @@ if __name__ == "__main__":
|
|||
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)
|
||||
if skip > 0:
|
||||
Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')
|
||||
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"))
|
||||
ppo_train_epoch(epoch, loader, len(loader) + skip, rollout_engine, ref_model, actor_scheduler, critic_scheduler, reward_model, start_step, wandb)
|
||||
else:
|
||||
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"))
|
||||
ppo_train_epoch(epoch, loader, len(loader), rollout_engine, ref_model, actor_scheduler, critic_scheduler, reward_model, 0, wandb)
|
||||
|
||||
# ========== 9. 清理分布进程 ==========
|
||||
if dist.is_initialized():
|
||||
|
|
|
|||
Loading…
Reference in New Issue