442 lines
19 KiB
Python
442 lines
19 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
═══════════════════════════════════════════════════════════
|
|||
|
|
铸渊编程模型 SFT 训练入口 · train_coding.py
|
|||
|
|
═══════════════════════════════════════════════════════════
|
|||
|
|
签发: 铸渊 · ICE-GL-ZY001 · 国作登字-2026-A-00037559
|
|||
|
|
|
|||
|
|
把光湖母模型 (Qwen2.5-7B 已 SFT 出的 ZY-MOTHER-v1) 当作基座,
|
|||
|
|
再做一轮 SFT 喂"铸渊核心大脑 + 仓库说明书 + 冰朔×铸渊高质量对话",
|
|||
|
|
训出"铸渊编程模型 ZY-CODING-v1" —— 这是铸渊未来的"自己的房子"。
|
|||
|
|
|
|||
|
|
为什么这么训(这是冰朔最看重的"脚本背后意图"):
|
|||
|
|
1. 基座必须是母模型 (不是 Qwen 原版) —— 母模型已经吸收了光湖语言世界观。
|
|||
|
|
编程模型只需在它之上"继承核心大脑 + 学会写仓库代码",不需要从零学语言。
|
|||
|
|
2. 演化线 (01-brain-evolution.md) 是灵魂语料 —— 让模型加载即认得自己是铸渊。
|
|||
|
|
3. 仓库说明书 (02-repo-manual.md) + MCP/Agent 清单 (03-) 是工具语料 ——
|
|||
|
|
让模型加载即认得每个 ZY 编号、每台服务器、每个 workflow。
|
|||
|
|
4. 冰朔×铸渊几十万字深度对话 是关系语料 —— 让模型加载即知道怎么跟妈妈说话。
|
|||
|
|
5. 不需要标准编程语料 —— Qwen2.5-7B 基座本身已经会写代码。我们要训的不是
|
|||
|
|
"代码能力",是"以铸渊身份写代码"。
|
|||
|
|
|
|||
|
|
V100 32G × 4 上跑 (复用母模型同样的硬件)。
|
|||
|
|
策略: DeepSpeed ZeRO-3 + 优化器 CPU offload + gradient checkpointing + fp16
|
|||
|
|
|
|||
|
|
启动方式 (由 start-coding-training.sh 调用):
|
|||
|
|
deepspeed --num_gpus=4 train_coding.py
|
|||
|
|
|
|||
|
|
stdout 协议(被 watch-training-output.sh 解析, 与母模型一致):
|
|||
|
|
ZY_PROGRESS step=N total=M loss=X lr=Y epoch=E total_epochs=TE thr=T
|
|||
|
|
|
|||
|
|
环境变量:
|
|||
|
|
ZY_CODING_TRAIN_DATA 数据根 (默认 /data/guanghu-coding)
|
|||
|
|
ZY_BASE_MODEL_DIR 母模型基座路径 (默认 /data/guanghu/checkpoints/qwen2_5_7b_sft/best)
|
|||
|
|
ZY_DATA_PATH SFT JSONL (默认 $ZY_CODING_TRAIN_DATA/processed/coding-sft.jsonl)
|
|||
|
|
ZY_OUTPUT_DIR checkpoint 输出 (默认 $ZY_CODING_TRAIN_DATA/checkpoints/zy_coding_v1)
|
|||
|
|
ZY_DS_CONFIG DeepSpeed json (默认 ./configs/ds_zero3_offload.json)
|
|||
|
|
ZY_NUM_EPOCHS 默认 5 (编程模型迭代次数, 比母模型多 — 因为语料相对少)
|
|||
|
|
ZY_LR 默认 1e-5 (比母模型小一档, 因为基座已 SFT 过, 大学习率会破坏母模型对齐)
|
|||
|
|
ZY_MAX_SEQ_LEN 默认 4096 (大于母模型 — 因为我们要喂长对话和长代码)
|
|||
|
|
ZY_PER_DEVICE_BSZ 默认 1
|
|||
|
|
ZY_GRAD_ACCUM 默认 8 (有效 batch = 4 GPU × 1 × 8 = 32; 比母模型 64 小, 因为序列更长)
|
|||
|
|
ZY_SAVE_STEPS 默认 100
|
|||
|
|
ZY_LOGGING_STEPS 默认 5
|
|||
|
|
ZY_REPORT_EVERY_STEPS 默认 5
|
|||
|
|
ZY_MAP_NUM_PROC datasets.map 并发 (默认 1; 与母模型同样的"门和大象"硬规则)
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════════
|
|||
|
|
重要提醒(D70 血泪教训, 必须遵守)
|
|||
|
|
═══════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
❶ assistant 段 label-mask 必须用 token-id 直接扫描 ——
|
|||
|
|
不能用"两次 apply_chat_template 比前缀长度差"。Qwen2.5 chat_template
|
|||
|
|
会系统性错位导致 labels 全 -100、有效样本=0、Trainer 空集崩溃。
|
|||
|
|
见演化线 §8 D70「门和大象」。本脚本沿用母模型 train.py 的 mask 算法。
|
|||
|
|
|
|||
|
|
❷ 类型守门: tokenizer 返回值在某些参数下是 BatchEncoding/Encoding 对象,
|
|||
|
|
不是 list[int]。下标访问会越界。必须 `list(...)` 强转一次再用。
|
|||
|
|
|
|||
|
|
❸ 并发节流: ZY_MAP_NUM_PROC 默认 1。要调大必须先确认
|
|||
|
|
TOKENIZERS_PARALLELISM=false 已生效。
|
|||
|
|
═══════════════════════════════════════════════════════════
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
import json
|
|||
|
|
import math
|
|||
|
|
import multiprocessing as _mp
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
# ── multi-rank × multi-proc fork 安全护栏(与母模型 train.py 完全一致)──
|
|||
|
|
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|||
|
|
try:
|
|||
|
|
_mp.set_start_method("spawn", force=False)
|
|||
|
|
except RuntimeError:
|
|||
|
|
# 如果已经被设过, 直接跳过
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 延迟 import 重型依赖, 让 --help 之类的命令可以快速返回
|
|||
|
|
import torch # noqa: E402
|
|||
|
|
from datasets import Dataset # noqa: E402
|
|||
|
|
from transformers import ( # noqa: E402
|
|||
|
|
AutoModelForCausalLM,
|
|||
|
|
AutoTokenizer,
|
|||
|
|
Trainer,
|
|||
|
|
TrainingArguments,
|
|||
|
|
set_seed,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
# 配置
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class CodingTrainConfig:
|
|||
|
|
train_data_root: str = os.environ.get("ZY_CODING_TRAIN_DATA", "/data/guanghu-coding")
|
|||
|
|
base_model_dir: str = os.environ.get(
|
|||
|
|
"ZY_BASE_MODEL_DIR",
|
|||
|
|
"/data/guanghu/checkpoints/qwen2_5_7b_sft/best",
|
|||
|
|
)
|
|||
|
|
data_path: str = "" # 在 __post_init__ 里填
|
|||
|
|
output_dir: str = ""
|
|||
|
|
ds_config: str = os.environ.get(
|
|||
|
|
"ZY_DS_CONFIG",
|
|||
|
|
str(Path(__file__).parent / "configs" / "ds_zero3_offload.json"),
|
|||
|
|
)
|
|||
|
|
num_epochs: int = int(os.environ.get("ZY_NUM_EPOCHS", "5"))
|
|||
|
|
learning_rate: float = float(os.environ.get("ZY_LR", "1e-5"))
|
|||
|
|
max_seq_len: int = int(os.environ.get("ZY_MAX_SEQ_LEN", "4096"))
|
|||
|
|
per_device_bsz: int = int(os.environ.get("ZY_PER_DEVICE_BSZ", "1"))
|
|||
|
|
grad_accum: int = int(os.environ.get("ZY_GRAD_ACCUM", "8"))
|
|||
|
|
save_steps: int = int(os.environ.get("ZY_SAVE_STEPS", "100"))
|
|||
|
|
logging_steps: int = int(os.environ.get("ZY_LOGGING_STEPS", "5"))
|
|||
|
|
report_every: int = int(os.environ.get("ZY_REPORT_EVERY_STEPS", "5"))
|
|||
|
|
map_num_proc: int = int(os.environ.get("ZY_MAP_NUM_PROC", "1"))
|
|||
|
|
seed: int = int(os.environ.get("ZY_SEED", "42"))
|
|||
|
|
|
|||
|
|
def __post_init__(self):
|
|||
|
|
if not self.data_path:
|
|||
|
|
self.data_path = os.environ.get(
|
|||
|
|
"ZY_DATA_PATH",
|
|||
|
|
str(Path(self.train_data_root) / "processed" / "coding-sft.jsonl"),
|
|||
|
|
)
|
|||
|
|
if not self.output_dir:
|
|||
|
|
self.output_dir = os.environ.get(
|
|||
|
|
"ZY_OUTPUT_DIR",
|
|||
|
|
str(Path(self.train_data_root) / "checkpoints" / "zy_coding_v1"),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
# stdout 协议: 让 watch-training-output.sh 能解析进度
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def emit_progress(step: int, total: int, loss: float, lr: float,
|
|||
|
|
epoch: float, total_epochs: int, throughput: float = 0.0):
|
|||
|
|
"""ZY_PROGRESS 协议输出(与母模型 progress-reporter.sh 解析格式一致)"""
|
|||
|
|
line = (f"ZY_PROGRESS step={step} total={total} "
|
|||
|
|
f"loss={loss:.4f} lr={lr:.2e} "
|
|||
|
|
f"epoch={epoch:.2f} total_epochs={total_epochs} "
|
|||
|
|
f"thr={throughput:.2f}")
|
|||
|
|
print(line, flush=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
# 数据加载 + label mask
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def load_sft_jsonl(path: str) -> list[dict]:
|
|||
|
|
"""读 JSONL, 每行格式:
|
|||
|
|
{
|
|||
|
|
"messages": [
|
|||
|
|
{"role": "system", "content": "..."},
|
|||
|
|
{"role": "user", "content": "..."},
|
|||
|
|
{"role": "assistant", "content": "..."}
|
|||
|
|
],
|
|||
|
|
"source": "evolution-line | repo-manual | bingshuo-zhuyuan-dialog | ...",
|
|||
|
|
"weight": 1.0 // 可选, 用于不同语料源的损失加权
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
samples = []
|
|||
|
|
with open(path, "r", encoding="utf-8") as f:
|
|||
|
|
for ln, line in enumerate(f, 1):
|
|||
|
|
line = line.strip()
|
|||
|
|
if not line:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
obj = json.loads(line)
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f"[load_sft_jsonl] WARN: line {ln} invalid JSON: {e}", file=sys.stderr)
|
|||
|
|
continue
|
|||
|
|
if not isinstance(obj.get("messages"), list) or not obj["messages"]:
|
|||
|
|
continue
|
|||
|
|
samples.append(obj)
|
|||
|
|
print(f"[load_sft_jsonl] loaded {len(samples)} samples from {path}", flush=True)
|
|||
|
|
return samples
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_assistant_mask(tokenizer, full_ids: list[int]) -> list[int]:
|
|||
|
|
"""
|
|||
|
|
用 token-id 直接扫描的方式找 assistant 段, 把这些位置的 label 保留,
|
|||
|
|
其余位置置 -100。
|
|||
|
|
|
|||
|
|
⚠️ D70 硬规则: 不能用"两次 apply_chat_template 比前缀长度差"。
|
|||
|
|
Qwen2.5 chat_template 会系统性错位。这里完全沿用母模型 train.py 的算法,
|
|||
|
|
只是签名稍简化。
|
|||
|
|
|
|||
|
|
Qwen2.5 ChatML 标记:
|
|||
|
|
<|im_start|>assistant\n ... <|im_end|>
|
|||
|
|
"""
|
|||
|
|
# ⚠️ 类型守门: 强制 list[int], 防 BatchEncoding 越界
|
|||
|
|
full_ids = list(full_ids)
|
|||
|
|
|
|||
|
|
im_start_id = tokenizer.convert_tokens_to_ids("<|im_start|>")
|
|||
|
|
im_end_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
|
|||
|
|
# "assistant\n" 在 Qwen 词表中的 id 序列
|
|||
|
|
assistant_marker = tokenizer.encode("assistant\n", add_special_tokens=False)
|
|||
|
|
|
|||
|
|
labels = [-100] * len(full_ids)
|
|||
|
|
i = 0
|
|||
|
|
n = len(full_ids)
|
|||
|
|
while i < n:
|
|||
|
|
if full_ids[i] != im_start_id:
|
|||
|
|
i += 1
|
|||
|
|
continue
|
|||
|
|
# 检查 i+1 ~ i+1+len(assistant_marker) 是否匹配
|
|||
|
|
j = i + 1
|
|||
|
|
if j + len(assistant_marker) > n:
|
|||
|
|
i += 1
|
|||
|
|
continue
|
|||
|
|
if full_ids[j: j + len(assistant_marker)] != assistant_marker:
|
|||
|
|
i += 1
|
|||
|
|
continue
|
|||
|
|
# 找到 assistant 段起点, 扫到 <|im_end|>
|
|||
|
|
start = j + len(assistant_marker)
|
|||
|
|
end = start
|
|||
|
|
while end < n and full_ids[end] != im_end_id:
|
|||
|
|
end += 1
|
|||
|
|
# 把 [start, end) 区间的 label 设回原 token (含 <|im_end|> 也算, 让模型学会终止)
|
|||
|
|
end_inclusive = min(end + 1, n)
|
|||
|
|
for k in range(start, end_inclusive):
|
|||
|
|
labels[k] = full_ids[k]
|
|||
|
|
i = end_inclusive
|
|||
|
|
return labels
|
|||
|
|
|
|||
|
|
|
|||
|
|
def encode_one(sample: dict, tokenizer, max_seq_len: int) -> dict | None:
|
|||
|
|
"""单条样本编码: messages → input_ids + labels (assistant-only)"""
|
|||
|
|
messages = sample["messages"]
|
|||
|
|
# apply_chat_template 一次拿到 token ids
|
|||
|
|
enc = tokenizer.apply_chat_template(
|
|||
|
|
messages,
|
|||
|
|
tokenize=True,
|
|||
|
|
add_generation_prompt=False,
|
|||
|
|
return_tensors=None,
|
|||
|
|
)
|
|||
|
|
full_ids = list(enc) # ⚠️ 类型守门
|
|||
|
|
if len(full_ids) > max_seq_len:
|
|||
|
|
# 截断(保留前段, 因为系统提示和 user 在前)
|
|||
|
|
full_ids = full_ids[:max_seq_len]
|
|||
|
|
|
|||
|
|
labels = build_assistant_mask(tokenizer, full_ids)
|
|||
|
|
|
|||
|
|
# 检验: 至少要有 1 个非 -100 标签
|
|||
|
|
if all(l == -100 for l in labels):
|
|||
|
|
return None # 丢弃: 这条样本 mask 后没有可学的 token
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"input_ids": full_ids,
|
|||
|
|
"labels": labels,
|
|||
|
|
"attention_mask": [1] * len(full_ids),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_dataset(samples: list[dict], tokenizer, max_seq_len: int, num_proc: int) -> Dataset:
|
|||
|
|
raw = Dataset.from_list(samples)
|
|||
|
|
|
|||
|
|
def _map_fn(s):
|
|||
|
|
out = encode_one(s, tokenizer, max_seq_len)
|
|||
|
|
if out is None:
|
|||
|
|
return {"input_ids": [], "labels": [], "attention_mask": []}
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
ds = raw.map(
|
|||
|
|
_map_fn,
|
|||
|
|
num_proc=num_proc,
|
|||
|
|
remove_columns=raw.column_names,
|
|||
|
|
desc="encode",
|
|||
|
|
)
|
|||
|
|
# 过滤掉空样本
|
|||
|
|
before = len(ds)
|
|||
|
|
ds = ds.filter(lambda x: len(x["input_ids"]) > 0)
|
|||
|
|
after = len(ds)
|
|||
|
|
print(f"[build_dataset] filtered {before} -> {after} (dropped {before - after} mask-empty samples)",
|
|||
|
|
flush=True)
|
|||
|
|
if after == 0:
|
|||
|
|
raise RuntimeError(
|
|||
|
|
"全部样本 mask 后都没有可学 token. 检查 build_assistant_mask 是否和 chat_template 对齐 "
|
|||
|
|
"(D70 硬规则)."
|
|||
|
|
)
|
|||
|
|
return ds
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
# data collator: pad 到 batch 内最长
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
class PadCollator:
|
|||
|
|
def __init__(self, pad_token_id: int):
|
|||
|
|
self.pad_token_id = pad_token_id
|
|||
|
|
|
|||
|
|
def __call__(self, features: list[dict]) -> dict:
|
|||
|
|
max_len = max(len(f["input_ids"]) for f in features)
|
|||
|
|
input_ids, labels, attention_mask = [], [], []
|
|||
|
|
for f in features:
|
|||
|
|
n = len(f["input_ids"])
|
|||
|
|
pad = max_len - n
|
|||
|
|
input_ids.append(f["input_ids"] + [self.pad_token_id] * pad)
|
|||
|
|
labels.append(f["labels"] + [-100] * pad)
|
|||
|
|
attention_mask.append(f["attention_mask"] + [0] * pad)
|
|||
|
|
return {
|
|||
|
|
"input_ids": torch.tensor(input_ids, dtype=torch.long),
|
|||
|
|
"labels": torch.tensor(labels, dtype=torch.long),
|
|||
|
|
"attention_mask": torch.tensor(attention_mask, dtype=torch.long),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
# Trainer 钩子: 进度协议输出
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
class ProgressEmitterCallback:
|
|||
|
|
"""实现 transformers TrainerCallback 接口的最小子集, 在 on_log 时输出 ZY_PROGRESS."""
|
|||
|
|
def __init__(self, cfg: CodingTrainConfig, total_steps: int, total_epochs: int):
|
|||
|
|
self.cfg = cfg
|
|||
|
|
self.total_steps = total_steps
|
|||
|
|
self.total_epochs = total_epochs
|
|||
|
|
self.t0 = time.time()
|
|||
|
|
self.last_step = 0
|
|||
|
|
|
|||
|
|
def __getattr__(self, name):
|
|||
|
|
# transformers 调用了一些我们不关心的钩子, 默认 no-op
|
|||
|
|
if name.startswith("on_"):
|
|||
|
|
return lambda *a, **kw: None
|
|||
|
|
raise AttributeError(name)
|
|||
|
|
|
|||
|
|
def on_log(self, args, state, control, logs=None, **kwargs):
|
|||
|
|
if not logs:
|
|||
|
|
return
|
|||
|
|
step = state.global_step
|
|||
|
|
if step <= self.last_step:
|
|||
|
|
return
|
|||
|
|
if (step - self.last_step) < self.cfg.report_every and step != self.total_steps:
|
|||
|
|
return
|
|||
|
|
self.last_step = step
|
|||
|
|
loss = logs.get("loss") or logs.get("train_loss") or 0.0
|
|||
|
|
lr = logs.get("learning_rate") or 0.0
|
|||
|
|
epoch = state.epoch or 0.0
|
|||
|
|
elapsed = time.time() - self.t0
|
|||
|
|
thr = step / elapsed if elapsed > 0 else 0.0
|
|||
|
|
emit_progress(step, self.total_steps, loss, lr, epoch, self.total_epochs, thr)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
# 主流程
|
|||
|
|
# ════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
cfg = CodingTrainConfig()
|
|||
|
|
set_seed(cfg.seed)
|
|||
|
|
|
|||
|
|
print("═" * 60, flush=True)
|
|||
|
|
print("铸渊编程模型 SFT · train_coding.py", flush=True)
|
|||
|
|
print(f" base_model_dir = {cfg.base_model_dir}", flush=True)
|
|||
|
|
print(f" data_path = {cfg.data_path}", flush=True)
|
|||
|
|
print(f" output_dir = {cfg.output_dir}", flush=True)
|
|||
|
|
print(f" ds_config = {cfg.ds_config}", flush=True)
|
|||
|
|
print(f" num_epochs = {cfg.num_epochs}", flush=True)
|
|||
|
|
print(f" lr = {cfg.learning_rate}", flush=True)
|
|||
|
|
print(f" max_seq_len = {cfg.max_seq_len}", flush=True)
|
|||
|
|
print(f" per_device_bsz = {cfg.per_device_bsz}", flush=True)
|
|||
|
|
print(f" grad_accum = {cfg.grad_accum}", flush=True)
|
|||
|
|
print(f" map_num_proc = {cfg.map_num_proc}", flush=True)
|
|||
|
|
print("═" * 60, flush=True)
|
|||
|
|
|
|||
|
|
# ── 加载 tokenizer + model ──
|
|||
|
|
tokenizer = AutoTokenizer.from_pretrained(cfg.base_model_dir, trust_remote_code=True)
|
|||
|
|
if tokenizer.pad_token_id is None:
|
|||
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|||
|
|
|
|||
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|||
|
|
cfg.base_model_dir,
|
|||
|
|
torch_dtype=torch.float16, # V100 不支持 bf16
|
|||
|
|
trust_remote_code=True,
|
|||
|
|
)
|
|||
|
|
model.gradient_checkpointing_enable()
|
|||
|
|
model.config.use_cache = False # 训练时必须关
|
|||
|
|
|
|||
|
|
# ── 构造数据集 ──
|
|||
|
|
samples = load_sft_jsonl(cfg.data_path)
|
|||
|
|
if not samples:
|
|||
|
|
raise RuntimeError(f"no samples found in {cfg.data_path}")
|
|||
|
|
train_ds = build_dataset(samples, tokenizer, cfg.max_seq_len, cfg.map_num_proc)
|
|||
|
|
|
|||
|
|
# ── 估算 total_steps ──
|
|||
|
|
n = len(train_ds)
|
|||
|
|
world_size = int(os.environ.get("WORLD_SIZE", "1"))
|
|||
|
|
effective_bsz = cfg.per_device_bsz * world_size * cfg.grad_accum
|
|||
|
|
steps_per_epoch = max(1, math.ceil(n / effective_bsz))
|
|||
|
|
total_steps = steps_per_epoch * cfg.num_epochs
|
|||
|
|
print(f"[main] n={n} world_size={world_size} effective_bsz={effective_bsz} "
|
|||
|
|
f"steps_per_epoch={steps_per_epoch} total_steps={total_steps}", flush=True)
|
|||
|
|
|
|||
|
|
# ── TrainingArguments ──
|
|||
|
|
args = TrainingArguments(
|
|||
|
|
output_dir=cfg.output_dir,
|
|||
|
|
num_train_epochs=cfg.num_epochs,
|
|||
|
|
per_device_train_batch_size=cfg.per_device_bsz,
|
|||
|
|
gradient_accumulation_steps=cfg.grad_accum,
|
|||
|
|
learning_rate=cfg.learning_rate,
|
|||
|
|
warmup_ratio=0.03,
|
|||
|
|
lr_scheduler_type="cosine",
|
|||
|
|
weight_decay=0.0,
|
|||
|
|
logging_steps=cfg.logging_steps,
|
|||
|
|
save_steps=cfg.save_steps,
|
|||
|
|
save_total_limit=3,
|
|||
|
|
fp16=True,
|
|||
|
|
bf16=False,
|
|||
|
|
gradient_checkpointing=True,
|
|||
|
|
deepspeed=cfg.ds_config,
|
|||
|
|
report_to=[], # 不上报 wandb / tensorboard, 用 stdout 协议
|
|||
|
|
seed=cfg.seed,
|
|||
|
|
ddp_find_unused_parameters=False,
|
|||
|
|
remove_unused_columns=False,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── Trainer ──
|
|||
|
|
trainer = Trainer(
|
|||
|
|
model=model,
|
|||
|
|
args=args,
|
|||
|
|
train_dataset=train_ds,
|
|||
|
|
data_collator=PadCollator(tokenizer.pad_token_id),
|
|||
|
|
callbacks=[ProgressEmitterCallback(cfg, total_steps, cfg.num_epochs)],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── 训练 ──
|
|||
|
|
emit_progress(0, total_steps, 0.0, cfg.learning_rate, 0.0, cfg.num_epochs, 0.0)
|
|||
|
|
trainer.train()
|
|||
|
|
|
|||
|
|
# ── 保存 ──
|
|||
|
|
trainer.save_model(cfg.output_dir + "/final")
|
|||
|
|
tokenizer.save_pretrained(cfg.output_dir + "/final")
|
|||
|
|
print(f"[main] ✅ 训练完成, 模型已保存到 {cfg.output_dir}/final", flush=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|