Compare commits

...

8 Commits

Author SHA1 Message Date
hy d8ff896a70
Merge 864bd67437 into 6fc918beb6 2026-09-04 22:30:24 +08:00
jingyaogong 6fc918beb6 [update] remove expert args 2026-09-04 22:16:38 +08:00
jingyaogong 4bd6062ead
Merge pull request #836 from JustinGuese/moe-cli-flags
Expose num_experts / num_experts_per_tok / seed on the trainer CLIs
2026-09-04 22:10:18 +08:00
jingyaogong 4d7a74acab
Merge pull request #840 from JustinGuese/readme-benchmark-resolution
docs: state the measurement resolution of the objective benchmark table
2026-09-04 22:10:00 +08:00
Justin Guese 77946fda00 [docs] 在客观评测表下补充各数据集的测量分辨率
lm_eval 对每个指标都会输出 stderr,但表格只保留了点估计。这里按测试集
大小给出各任务的 ± 标准误(obqa ±1.9 到 cmmlu ±0.4),并说明比较两个
模型时差值的标准误约为单项的 √2 倍,方便读者判断表中哪些差距是可以
解读的。中英文 README 同步。
2026-09-03 17:10:52 +07:00
Justin Guese fc585a9bf9 [update] 把随机种子暴露到命令行
trainer 里的种子此前硬编码为 42(setup_seed(42 + rank) 与每轮的
setup_seed(42 + epoch)),要跑多种子实验只能改源码。新增 --seed,
默认 42,与改动前完全一致。
2026-09-03 15:36:14 +07:00
Justin Guese c3199c7240 [update] 把 num_experts / num_experts_per_tok 暴露到命令行
此前要改专家数只能去编辑 model_minimind.py。三个入口
(train_pretrain / train_full_sft / eval_llm)的默认值与 MiniMindConfig
一致(4 / 1),默认行为不变;RL 与 agent 相关的 trainer 不动。
2026-09-03 15:13:29 +07:00
liuhy495 864bd67437 feat: add generalized on-policy distillation trainer 2026-07-15 21:15:10 +08:00
7 changed files with 955 additions and 9 deletions

View File

@ -1599,6 +1599,15 @@ MiniMind 的数据规模远小于表中其他模型,且训练比例偏向中
| [SmolLM2-135M](https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct) | HuggingFace | 135M | 24.44 / 24.71 | 58.50 / 68.17 / 32.80 / 43.15 / 39.46 | | [SmolLM2-135M](https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct) | HuggingFace | 135M | 24.44 / 24.71 | 58.50 / 68.17 / 32.80 / 43.15 / 39.46 |
| [Aquila-135M](https://huggingface.co/BAAI/Aquila-135M-Instruct) | BAAI | 135M | 25.19 / 25.10 | 54.59 / 67.52 / 34.40 / 41.67 / 39.66 | | [Aquila-135M](https://huggingface.co/BAAI/Aquila-135M-Instruct) | BAAI | 135M | 25.19 / 25.10 | 54.59 / 67.52 / 34.40 / 41.67 / 39.66 |
> **这张表的分辨率**:`lm_eval` 对每个指标都会同时输出一个 `stderr`,它主要由测试集大小决定,因此下面的数值对表中所有模型都近似适用(随准确率略有浮动):
>
> | 数据集 | obqa | ceval | piqa | siqa | arc | hellaswag | cmmlu |
> |---|---|---|---|---|---|---|---|
> | 样本量 | 500 | ~1.3k | 1838 | 1954 | 2376 | 10042 | 11582 |
> | ± 标准误 | **1.9** | 1.2 | 1.2 | 1.1 | 0.9 | 0.5 | **0.4** |
>
> 比较两个模型时,差值的标准误约为单项的 `√2` 倍。因此 obqa 上小于约 5 个百分点、ceval / piqa / siqa 上小于约 3 个百分点的差异,都不足以区分两个模型——这也解释了为什么本表中同一量级的模型排序不宜过度解读。
<details> <details>
<summary><strong>补充说明(来源/无污染/复现)</strong></summary> <summary><strong>补充说明(来源/无污染/复现)</strong></summary>

View File

@ -1597,6 +1597,15 @@ MiniMind is trained on far less data than the other models listed here, and its
| [SmolLM2-135M](https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct) | HuggingFace | 135M | 24.44 / 24.71 | 58.50 / 68.17 / 32.80 / 43.15 / 39.46 | | [SmolLM2-135M](https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct) | HuggingFace | 135M | 24.44 / 24.71 | 58.50 / 68.17 / 32.80 / 43.15 / 39.46 |
| [Aquila-135M](https://huggingface.co/BAAI/Aquila-135M-Instruct) | BAAI | 135M | 25.19 / 25.10 | 54.59 / 67.52 / 34.40 / 41.67 / 39.66 | | [Aquila-135M](https://huggingface.co/BAAI/Aquila-135M-Instruct) | BAAI | 135M | 25.19 / 25.10 | 54.59 / 67.52 / 34.40 / 41.67 / 39.66 |
> **The resolution of this table**: `lm_eval` reports a `stderr` alongside every metric. It is set mainly by the size of the test set, so the values below apply approximately to every model in the table (they shift slightly with accuracy):
>
> | dataset | obqa | ceval | piqa | siqa | arc | hellaswag | cmmlu |
> |---|---|---|---|---|---|---|---|
> | samples | 500 | ~1.3k | 1838 | 1954 | 2376 | 10042 | 11582 |
> | ± stderr | **1.9** | 1.2 | 1.2 | 1.1 | 0.9 | 0.5 | **0.4** |
>
> When comparing two models, the standard error of the difference is about `√2` times the single-model value. So a gap smaller than roughly 5 points on obqa, or roughly 3 points on ceval / piqa / siqa, does not separate two models — which is why the ordering of similarly-sized models in this table should not be over-read.
<details> <details>
<summary><strong>Additional note (source / no contamination / reproduction)</strong></summary> <summary><strong>Additional note (source / no contamination / reproduction)</strong></summary>

148
trainer/opd_utils.py Normal file
View File

@ -0,0 +1,148 @@
"""Core tensor utilities for Generalized Knowledge Distillation (GKD)."""
from typing import Dict, Optional, Tuple
import torch
import torch.nn.functional as F
from torch import Tensor
def build_completion_mask(
completion_ids: Tensor,
base_mask: Optional[Tensor],
eos_token_id: Optional[int],
pad_token_id: Optional[int] = None,
) -> Tensor:
"""Keep valid completion tokens up to and including the first EOS.
MiniMind's native ``generate`` pads finished rows with repeated EOS tokens
until every row is complete. Those repeated tokens are valid model inputs,
but they must not contribute to the distillation objective.
"""
if completion_ids.ndim != 2:
raise ValueError(f"completion_ids must be 2D, got {tuple(completion_ids.shape)}")
if base_mask is None:
mask = torch.ones_like(completion_ids, dtype=torch.bool)
else:
if base_mask.shape != completion_ids.shape:
raise ValueError(
f"base_mask shape {tuple(base_mask.shape)} does not match "
f"completion_ids shape {tuple(completion_ids.shape)}"
)
mask = base_mask.bool().clone()
# Some tokenizers intentionally use EOS as PAD. In that case the first EOS
# still belongs to the completion and is handled by the cumulative mask.
if pad_token_id is not None and pad_token_id != eos_token_id:
mask &= completion_ids.ne(pad_token_id)
if eos_token_id is not None:
eos_hits = completion_ids.eq(eos_token_id) & mask
eos_seen_before = eos_hits.long().cumsum(dim=1) - eos_hits.long()
mask &= eos_seen_before.eq(0)
return mask
def _masked_mean(values: Tensor, mask: Tensor) -> Tensor:
mask = mask.to(device=values.device, dtype=values.dtype)
return (values * mask).sum() / mask.sum().clamp_min(1.0)
def generalized_jsd_loss(
student_logits: Tensor,
teacher_logits: Tensor,
completion_mask: Tensor,
beta: float = 0.5,
temperature: float = 1.0,
) -> Tuple[Tensor, Dict[str, Tensor]]:
"""Compute the exact full-vocabulary generalized JSD from GKD.
With teacher distribution ``p`` and student distribution ``q``, the
mixture is ``m = beta * p + (1 - beta) * q`` and the objective is
``beta * KL(p || m) + (1 - beta) * KL(q || m)``. Its limiting cases are
forward KL at ``beta=0`` and reverse KL at ``beta=1``.
The teacher is always treated as a fixed target. Only completion tokens
selected by ``completion_mask`` contribute to the token-mean loss.
"""
if student_logits.ndim != 3 or teacher_logits.ndim != 3:
raise ValueError("student_logits and teacher_logits must be [batch, time, vocab]")
if student_logits.shape != teacher_logits.shape:
raise ValueError(
"student and teacher logits must have identical shapes, got "
f"{tuple(student_logits.shape)} and {tuple(teacher_logits.shape)}"
)
if completion_mask.shape != student_logits.shape[:2]:
raise ValueError(
f"completion_mask shape {tuple(completion_mask.shape)} does not match "
f"logits shape {tuple(student_logits.shape[:2])}"
)
if not 0.0 <= beta <= 1.0:
raise ValueError(f"beta must be in [0, 1], got {beta}")
if temperature <= 0:
raise ValueError(f"temperature must be positive, got {temperature}")
# Float32 log-softmax keeps the divergence stable under fp16/bf16 model
# autocast, while preserving gradients back to the student parameters.
student_logps = F.log_softmax(student_logits.float() / temperature, dim=-1)
teacher_logps = F.log_softmax(
teacher_logits.detach().float() / temperature, dim=-1
)
student_probs = student_logps.exp()
teacher_probs = teacher_logps.exp()
forward_kl = (teacher_probs * (teacher_logps - student_logps)).sum(dim=-1)
reverse_kl = (student_probs * (student_logps - teacher_logps)).sum(dim=-1)
if beta == 0.0:
per_token_loss = forward_kl
elif beta == 1.0:
per_token_loss = reverse_kl
else:
beta_tensor = student_logps.new_tensor(beta)
mixture_logps = torch.logsumexp(
torch.stack(
(
student_logps + torch.log1p(-beta_tensor),
teacher_logps + torch.log(beta_tensor),
)
),
dim=0,
)
teacher_to_mixture = (
teacher_probs * (teacher_logps - mixture_logps)
).sum(dim=-1)
student_to_mixture = (
student_probs * (student_logps - mixture_logps)
).sum(dim=-1)
per_token_loss = (
beta_tensor * teacher_to_mixture
+ (1.0 - beta_tensor) * student_to_mixture
)
mask = completion_mask.bool()
loss = _masked_mean(per_token_loss, mask)
top1_agreement = _masked_mean(
student_logps.detach()
.argmax(dim=-1)
.eq(teacher_logps.argmax(dim=-1))
.float(),
mask,
)
metrics = {
"divergence": loss.detach(),
"forward_kl": _masked_mean(forward_kl.detach(), mask),
"reverse_kl": _masked_mean(reverse_kl.detach(), mask),
"top1_agreement": top1_agreement.detach(),
"valid_tokens": mask.sum().detach(),
}
return loss, metrics
def use_on_policy_batch(lmbda: float, random_value: float) -> bool:
"""Return the Algorithm 1 branch for a pre-sampled value in ``[0, 1)``."""
if not 0.0 <= lmbda <= 1.0:
raise ValueError(f"lmbda must be in [0, 1], got {lmbda}")
if not 0.0 <= random_value < 1.0:
raise ValueError(f"random_value must be in [0, 1), got {random_value}")
return random_value < lmbda

View File

@ -52,7 +52,10 @@ class RolloutEngine(ABC):
tokenizer = None tokenizer = None
@abstractmethod @abstractmethod
def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int, max_new_tokens: int, temperature: float = 0.8) -> RolloutResult: def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int,
max_new_tokens: int, temperature: float = 0.8,
top_p: Optional[float] = None, top_k: Optional[int] = None,
calculate_logps: bool = True) -> RolloutResult:
pass pass
@abstractmethod @abstractmethod
@ -68,9 +71,15 @@ class TorchRolloutEngine(RolloutEngine):
self.device = device self.device = device
self.autocast_ctx = autocast_ctx self.autocast_ctx = autocast_ctx
def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int, max_new_tokens: int, temperature: float = 0.8) -> RolloutResult: def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int,
max_new_tokens: int, temperature: float = 0.8,
top_p: Optional[float] = None, top_k: Optional[int] = None,
calculate_logps: bool = True) -> RolloutResult:
model = self.policy_model.module if isinstance(self.policy_model, DistributedDataParallel) else self.policy_model model = self.policy_model.module if isinstance(self.policy_model, DistributedDataParallel) else self.policy_model
ctx = self.autocast_ctx if self.autocast_ctx else nullcontext() ctx = self.autocast_ctx if self.autocast_ctx else nullcontext()
sampling_kwargs = {}
if top_p is not None: sampling_kwargs['top_p'] = top_p
if top_k is not None: sampling_kwargs['top_k'] = top_k
with torch.no_grad(), ctx: with torch.no_grad(), ctx:
output_ids = model.generate( output_ids = model.generate(
input_ids=prompt_ids.repeat_interleave(num_generations, dim=0), input_ids=prompt_ids.repeat_interleave(num_generations, dim=0),
@ -81,11 +90,20 @@ class TorchRolloutEngine(RolloutEngine):
num_return_sequences=1, num_return_sequences=1,
pad_token_id=self.tokenizer.pad_token_id, pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self.tokenizer.eos_token_id, eos_token_id=self.tokenizer.eos_token_id,
**sampling_kwargs,
).clone() # [B*num_gen, P+R] ).clone() # [B*num_gen, P+R]
prompt_len = prompt_ids.size(1) prompt_len = prompt_ids.size(1)
completion_ids = output_ids[:, prompt_len:] # [B*num_gen, R] completion_ids = output_ids[:, prompt_len:] # [B*num_gen, R]
full_mask = (output_ids != self.tokenizer.pad_token_id).long() full_mask = (output_ids != self.tokenizer.pad_token_id).long()
per_token_logps = compute_per_token_logps(self.policy_model, output_ids, completion_ids.size(1), attention_mask=full_mask) if calculate_logps:
per_token_logps = compute_per_token_logps(
self.policy_model, output_ids, completion_ids.size(1), attention_mask=full_mask
)
else:
per_token_logps = torch.zeros(
output_ids.size(0), completion_ids.size(1),
device=output_ids.device, dtype=torch.float32
)
completions = self.tokenizer.batch_decode(completion_ids, skip_special_tokens=True) completions = self.tokenizer.batch_decode(completion_ids, skip_special_tokens=True)
return RolloutResult(output_ids, completion_ids, per_token_logps, completions, return RolloutResult(output_ids, completion_ids, per_token_logps, completions,
prompt_ids.new_full((output_ids.size(0),), prompt_len), prompt_ids.new_full((output_ids.size(0),), prompt_len),
@ -104,7 +122,10 @@ class SGLangRolloutEngine(RolloutEngine):
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
self.http = requests self.http = requests
def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int, max_new_tokens: int, temperature: float = 0.8) -> RolloutResult: def rollout(self, prompt_ids: Tensor, attention_mask: Tensor, num_generations: int,
max_new_tokens: int, temperature: float = 0.8,
top_p: Optional[float] = None, top_k: Optional[int] = None,
calculate_logps: bool = True) -> RolloutResult:
# 去除左侧 padding tokens,只保留有效 token # 去除左侧 padding tokens,只保留有效 token
input_ids_list = [] input_ids_list = []
for ids, mask in zip(prompt_ids, attention_mask): for ids, mask in zip(prompt_ids, attention_mask):
@ -119,8 +140,10 @@ class SGLangRolloutEngine(RolloutEngine):
"max_new_tokens": max_new_tokens, "max_new_tokens": max_new_tokens,
"stop_token_ids": [self.tokenizer.eos_token_id] if self.tokenizer.eos_token_id else [], "stop_token_ids": [self.tokenizer.eos_token_id] if self.tokenizer.eos_token_id else [],
}, },
"return_logprob": True, "return_logprob": calculate_logps,
} }
if top_p is not None: payload['sampling_params']['top_p'] = top_p
if top_k is not None: payload['sampling_params']['top_k'] = top_k if top_k > 0 else -1
resp = self.http.post(f"{self.base_url}/generate", json=payload, timeout=self.timeout) resp = self.http.post(f"{self.base_url}/generate", json=payload, timeout=self.timeout)
resp.raise_for_status() resp.raise_for_status()

View File

@ -99,6 +99,7 @@ if __name__ == "__main__":
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量") parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
parser.add_argument('--max_seq_len', default=768, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)") parser.add_argument('--max_seq_len', default=768, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)") parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
parser.add_argument('--seed', default=42, type=int, help="随机种子(DDP下每个rank为seed+rank,每轮为seed+epoch)")
parser.add_argument("--data_path", type=str, default="../dataset/sft_t2t_mini.jsonl", help="训练数据路径") parser.add_argument("--data_path", type=str, default="../dataset/sft_t2t_mini.jsonl", help="训练数据路径")
parser.add_argument('--from_weight', default='pretrain', type=str, help="基于哪个权重训练,为none则不基于任何权重训练") parser.add_argument('--from_weight', default='pretrain', type=str, help="基于哪个权重训练,为none则不基于任何权重训练")
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)") parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
@ -110,7 +111,7 @@ if __name__ == "__main__":
# ========== 1. 初始化环境和随机种子 ========== # ========== 1. 初始化环境和随机种子 ==========
local_rank = init_distributed_mode() local_rank = init_distributed_mode()
if dist.is_initialized(): args.device = f"cuda:{local_rank}" if dist.is_initialized(): args.device = f"cuda:{local_rank}"
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0)) setup_seed(args.seed + (dist.get_rank() if dist.is_initialized() else 0))
# ========== 2. 配置目录、模型参数、检查ckp ========== # ========== 2. 配置目录、模型参数、检查ckp ==========
os.makedirs(args.save_dir, exist_ok=True) os.makedirs(args.save_dir, exist_ok=True)
@ -157,7 +158,7 @@ if __name__ == "__main__":
# ========== 8. 开始训练 ========== # ========== 8. 开始训练 ==========
for epoch in range(start_epoch, args.epochs): for epoch in range(start_epoch, args.epochs):
train_sampler and train_sampler.set_epoch(epoch) train_sampler and train_sampler.set_epoch(epoch)
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist() setup_seed(args.seed + epoch); indices = torch.randperm(len(train_ds)).tolist()
skip = start_step if (epoch == start_epoch and start_step > 0) else 0 skip = start_step if (epoch == start_epoch and start_step > 0) else 0
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip) batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True) loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)

755
trainer/train_opd.py Normal file
View File

@ -0,0 +1,755 @@
import os
import sys
__package__ = "trainer"
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import datasets # noqa: F401 # Windows pyarrow/torch DLL conflict workaround (issue #771)
import argparse
import gc
import json
import math
import random
import warnings
from contextlib import nullcontext
from pathlib import Path
import torch
import torch.distributed as dist
from datasets import load_dataset
from torch import optim
from torch.nn.parallel import DistributedDataParallel
from torch.optim.lr_scheduler import CosineAnnealingLR
from torch.utils.data import DataLoader, Dataset, DistributedSampler
from transformers import AutoTokenizer
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from trainer.opd_utils import (
build_completion_mask,
generalized_jsd_loss,
use_on_policy_batch,
)
from trainer.rollout_engine import create_rollout_engine
from trainer.trainer_utils import (
Logger,
SkipBatchSampler,
init_distributed_mode,
is_main_process,
lm_checkpoint,
setup_seed,
)
warnings.filterwarnings("ignore")
REPO_ROOT = Path(__file__).resolve().parents[1]
class OPDDataset(Dataset):
"""Raw conversational samples used by both GKD data branches."""
def __init__(self, data_path):
self.samples = load_dataset("json", data_files=str(data_path), split="train")
def __len__(self):
return len(self.samples)
def __getitem__(self, index):
sample = self.samples[index]
conversations = sample.get("conversations", sample.get("messages"))
if not isinstance(conversations, list) or not conversations:
raise ValueError(
"OPD data requires a non-empty 'conversations' or 'messages' list"
)
return {"conversations": [dict(message) for message in conversations]}
def collate_conversations(batch):
return {"conversations": [item["conversations"] for item in batch]}
def resolve_device(device):
if device != "auto":
return device
if torch.cuda.is_available():
return "cuda:0"
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
return "mps"
return "cpu"
def resolve_path(value):
path = Path(value).expanduser()
if path.is_absolute():
return path.resolve()
return (REPO_ROOT / path).resolve()
def validate_args(parsed_args):
if not 0.0 <= parsed_args.lmbda <= 1.0:
raise ValueError("--lmbda must be in [0, 1]")
if not 0.0 <= parsed_args.beta <= 1.0:
raise ValueError("--beta must be in [0, 1]")
if parsed_args.temperature <= 0 or parsed_args.distill_temperature <= 0:
raise ValueError("sampling and distillation temperatures must be positive")
if not 0.0 < parsed_args.rollout_top_p <= 1.0:
raise ValueError("--rollout_top_p must be in (0, 1]")
if parsed_args.rollout_top_k < 0:
raise ValueError("--rollout_top_k must be zero (disabled) or positive")
if parsed_args.num_generations < 1 or parsed_args.max_gen_len < 1:
raise ValueError("--num_generations and --max_gen_len must be positive")
if parsed_args.max_seq_len < 1 or parsed_args.accumulation_steps < 1:
raise ValueError("--max_seq_len and --accumulation_steps must be positive")
if parsed_args.max_train_steps < 0:
raise ValueError("--max_train_steps must be zero or positive")
if parsed_args.log_interval < 1 or parsed_args.save_interval < 1:
raise ValueError("log/save intervals must be positive")
if parsed_args.rollout_top_p < 1.0 or parsed_args.rollout_top_k > 0:
Logger(
"[OPD WARNING] top-p/top-k truncation changes the behavior policy; "
"top_p=1 and top_k=0 match the GKD baseline"
)
def _torch_load(path):
try:
return torch.load(path, map_location="cpu", weights_only=True)
except TypeError:
return torch.load(path, map_location="cpu")
def _load_transformers_state_dict(model_dir):
single_safetensors = model_dir / "model.safetensors"
safetensors_index = model_dir / "model.safetensors.index.json"
pytorch_bin = model_dir / "pytorch_model.bin"
if single_safetensors.exists() or safetensors_index.exists():
try:
from safetensors.torch import load_file
except ImportError as exc:
raise ImportError(
"Loading a Transformers safetensors model requires safetensors"
) from exc
if single_safetensors.exists():
return load_file(str(single_safetensors), device="cpu")
if safetensors_index.exists():
index = json.loads(safetensors_index.read_text(encoding="utf-8"))
state_dict = {}
for shard_name in sorted(set(index["weight_map"].values())):
state_dict.update(load_file(str(model_dir / shard_name), device="cpu"))
return state_dict
if pytorch_bin.exists():
return _torch_load(pytorch_bin)
raise FileNotFoundError(
f"No model.safetensors, model.safetensors.index.json, or pytorch_model.bin in {model_dir}"
)
def resolve_model_source(source, config):
direct = Path(source).expanduser()
if not direct.is_absolute():
direct = REPO_ROOT / direct
if direct.exists():
return direct.resolve()
moe_suffix = "_moe" if config.use_moe else ""
weight_path = REPO_ROOT / "out" / f"{source}_{config.hidden_size}{moe_suffix}.pth"
if weight_path.exists():
return weight_path.resolve()
raise FileNotFoundError(
f"Model source {source!r} was not found directly or as {weight_path}"
)
def load_state_dict(source, config):
source = resolve_model_source(source, config)
state_dict = (
_load_transformers_state_dict(source) if source.is_dir() else _torch_load(source)
)
if isinstance(state_dict, dict) and isinstance(state_dict.get("model"), dict):
state_dict = state_dict["model"]
if not isinstance(state_dict, dict) or not state_dict:
raise ValueError(f"Invalid or empty state dict from {source}")
for prefix in ("module.", "_orig_mod."):
if all(key.startswith(prefix) for key in state_dict):
state_dict = {key[len(prefix):]: value for key, value in state_dict.items()}
if "lm_head.weight" not in state_dict and "model.embed_tokens.weight" in state_dict:
state_dict["lm_head.weight"] = state_dict["model.embed_tokens.weight"]
if "model.embed_tokens.weight" not in state_dict and "lm_head.weight" in state_dict:
state_dict["model.embed_tokens.weight"] = state_dict["lm_head.weight"]
return state_dict, source
def validate_directory_config(source, config, label):
source_path = Path(source).expanduser()
if not source_path.is_absolute():
source_path = REPO_ROOT / source_path
config_path = source_path / "config.json" if source_path.is_dir() else None
if not config_path or not config_path.exists():
return
saved = json.loads(config_path.read_text(encoding="utf-8"))
expected = {
"hidden_size": config.hidden_size,
"num_hidden_layers": config.num_hidden_layers,
"vocab_size": config.vocab_size,
"num_attention_heads": config.num_attention_heads,
"num_key_value_heads": config.num_key_value_heads,
}
mismatches = {
key: (saved.get(key), value)
for key, value in expected.items()
if saved.get(key) is not None and saved.get(key) != value
}
if mismatches:
raise ValueError(f"{label} config does not match CLI architecture: {mismatches}")
def load_native_model(config, source, device, label, model_dtype=None):
validate_directory_config(source, config, label)
state_dict, resolved_source = load_state_dict(source, config)
loaded_model = MiniMindForCausalLM(config)
loaded_model.load_state_dict(state_dict, strict=True)
del state_dict
gc.collect()
if model_dtype is None:
loaded_model = loaded_model.to(device)
else:
loaded_model = loaded_model.to(device=device, dtype=model_dtype)
Logger(f"Loaded {label} strictly from {resolved_source}")
return loaded_model
def load_tokenizer(tokenizer_path, vocab_size):
tokenizer = AutoTokenizer.from_pretrained(
str(resolve_path(tokenizer_path)), trust_remote_code=True
)
if len(tokenizer) != vocab_size:
raise ValueError(
f"Tokenizer size {len(tokenizer)} does not match model vocab {vocab_size}"
)
if tokenizer.pad_token_id is None or tokenizer.eos_token_id is None:
raise ValueError("OPD requires tokenizer.pad_token_id and tokenizer.eos_token_id")
tokenizer.padding_side = "left"
tokenizer.truncation_side = "left"
return tokenizer
def _parse_json_field(value):
if not isinstance(value, str):
return value
try:
return json.loads(value)
except json.JSONDecodeError:
return value
def render_sample(tokenizer, conversations):
"""Render one prompt and its optional fixed completion."""
messages = []
tools = None
for raw_message in conversations:
message = {key: value for key, value in dict(raw_message).items() if value is not None}
if message.get("tools"):
parsed_tools = _parse_json_field(message["tools"])
if isinstance(parsed_tools, list):
tools = parsed_tools
if message.get("tool_calls"):
message["tool_calls"] = _parse_json_field(message["tool_calls"])
messages.append(message)
has_completion = messages[-1].get("role") == "assistant" and any(
messages[-1].get(field) for field in ("content", "reasoning_content", "tool_calls")
)
prompt_messages = messages[:-1] if messages[-1].get("role") == "assistant" else messages
template_kwargs = {"tools": tools} if tools else {}
prompt_text = tokenizer.apply_chat_template(
prompt_messages,
tokenize=False,
add_generation_prompt=True,
**template_kwargs,
)
full_text = None
if has_completion:
full_text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
**template_kwargs,
)
return prompt_text, full_text
def tokenize_prompts(tokenizer, prompt_texts, max_seq_len, device):
encoded = tokenizer(
prompt_texts,
add_special_tokens=False,
return_tensors="pt",
padding=True,
truncation=True,
max_length=max_seq_len,
return_token_type_ids=False,
)
return {key: value.to(device) for key, value in encoded.items()}
def build_off_policy_batch(
tokenizer, prompt_texts, full_texts, max_seq_len, max_gen_len, device
):
"""Tokenize fixed prompt-completion pairs and mask all prompt tokens."""
sequences = []
label_masks = []
for prompt_text, full_text in zip(prompt_texts, full_texts):
if full_text is None:
raise ValueError(
"Off-policy GKD selected a sample without a fixed assistant completion; "
"use --lmbda 1 for prompt-only data"
)
prompt_ids = tokenizer(prompt_text, add_special_tokens=False).input_ids
full_ids = tokenizer(full_text, add_special_tokens=False).input_ids
if full_ids[:len(prompt_ids)] != prompt_ids:
raise ValueError(
"The rendered prompt is not a token prefix of the fixed conversation; "
"the tokenizer/chat template is incompatible with prompt-completion GKD"
)
completion_ids = full_ids[len(prompt_ids):]
prompt_ids = prompt_ids[-max_seq_len:]
completion_ids = completion_ids[:max_gen_len]
if not completion_ids:
raise ValueError("A fixed completion became empty after tokenization/truncation")
sequences.append(prompt_ids + completion_ids)
label_masks.append([False] * len(prompt_ids) + [True] * len(completion_ids))
max_length = max(len(sequence) for sequence in sequences)
input_ids = []
attention_mask = []
labels = []
for sequence, label_mask in zip(sequences, label_masks):
padding = max_length - len(sequence)
input_ids.append(sequence + [tokenizer.pad_token_id] * padding)
attention_mask.append([1] * len(sequence) + [0] * padding)
labels.append(
[token if keep else -100 for token, keep in zip(sequence, label_mask)]
+ [-100] * padding
)
return (
torch.tensor(input_ids, dtype=torch.long, device=device),
torch.tensor(attention_mask, dtype=torch.long, device=device),
torch.tensor(labels, dtype=torch.long, device=device),
)
def distributed_mean(value):
result = value.detach().float().clone()
if dist.is_initialized():
dist.all_reduce(result, op=dist.ReduceOp.SUM)
result /= dist.get_world_size()
return result.item()
def choose_on_policy(lmbda, device):
if dist.is_initialized():
draw = torch.zeros((), device=device)
if dist.get_rank() == 0:
draw.fill_(random.random())
dist.broadcast(draw, src=0)
random_value = draw.item()
else:
random_value = random.random()
return use_on_policy_batch(lmbda, random_value)
def save_student(lm_config, epoch, step, wandb):
if not is_main_process():
return
model.eval()
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
raw_model = getattr(raw_model, "_orig_mod", raw_model)
moe_suffix = "_moe" if lm_config.use_moe else ""
output_path = resolve_path(args.save_dir) / (
f"{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth"
)
state_dict = raw_model.state_dict()
torch.save({key: value.half().cpu() for key, value in state_dict.items()}, output_path)
lm_checkpoint(
lm_config,
weight=args.save_weight,
model=model,
optimizer=optimizer,
epoch=epoch,
step=step,
wandb=wandb,
save_dir=str(resolve_path(args.checkpoint_dir)),
scheduler=scheduler,
scaler=scaler,
)
model.train()
del state_dict
def gkd_train_epoch(
epoch, loader, iters, rollout_engine, teacher_model, lm_config,
start_step=0, wandb=None
):
teacher_model.eval().requires_grad_(False)
model.train()
on_policy_batches = 0
processed_batches = 0
for step, batch in enumerate(loader, start=start_step + 1):
if args.max_train_steps > 0 and processed_batches >= args.max_train_steps:
break
processed_batches += 1
on_policy = choose_on_policy(args.lmbda, args.device)
on_policy_batches += int(on_policy)
rendered = [render_sample(tokenizer, item) for item in batch["conversations"]]
prompt_texts = [item[0] for item in rendered]
full_texts = [item[1] for item in rendered]
if on_policy:
prompt_inputs = tokenize_prompts(
tokenizer, prompt_texts, args.max_seq_len, args.device
)
model.eval()
rollout_result = rollout_engine.rollout(
prompt_ids=prompt_inputs["input_ids"],
attention_mask=prompt_inputs["attention_mask"],
num_generations=args.num_generations,
max_new_tokens=args.max_gen_len,
temperature=args.temperature,
top_p=args.rollout_top_p,
top_k=args.rollout_top_k,
calculate_logps=False,
)
model.train()
input_ids = rollout_result.output_ids.to(args.device)
completion_ids = rollout_result.completion_ids.to(args.device)
completion_mask = build_completion_mask(
completion_ids,
rollout_result.completion_mask.to(args.device),
eos_token_id=tokenizer.eos_token_id,
pad_token_id=tokenizer.pad_token_id,
)
prompt_mask = prompt_inputs["attention_mask"].repeat_interleave(
args.num_generations, dim=0
)
attention_mask = torch.cat(
(prompt_mask, completion_mask.to(prompt_mask.dtype)), dim=1
)
with autocast_ctx:
student_output = model(
input_ids,
attention_mask=attention_mask,
logits_to_keep=completion_ids.size(1) + 1,
)
student_logits = student_output.logits[:, :-1, :]
with torch.no_grad(), autocast_ctx:
teacher_logits = teacher_model(
input_ids,
attention_mask=attention_mask,
logits_to_keep=completion_ids.size(1) + 1,
).logits[:, :-1, :]
avg_completion_length = completion_mask.sum(dim=1).float().mean()
debug_completion = rollout_result.completions[0]
else:
input_ids, attention_mask, labels = build_off_policy_batch(
tokenizer,
prompt_texts,
full_texts,
args.max_seq_len,
args.max_gen_len,
args.device,
)
completion_mask = labels[:, 1:].ne(-100)
with autocast_ctx:
student_output = model(input_ids, attention_mask=attention_mask)
student_logits = student_output.logits[:, :-1, :]
with torch.no_grad(), autocast_ctx:
teacher_logits = teacher_model(
input_ids, attention_mask=attention_mask
).logits[:, :-1, :]
avg_completion_length = completion_mask.sum(dim=1).float().mean()
debug_completion = full_texts[0]
gkd_loss, gkd_metrics = generalized_jsd_loss(
student_logits,
teacher_logits,
completion_mask,
beta=args.beta,
temperature=args.distill_temperature,
)
aux_loss = student_output.aux_loss if lm_config.use_moe else gkd_loss.new_zeros(())
total_loss = gkd_loss + aux_loss
scaler.scale(total_loss / args.accumulation_steps).backward()
should_step = step % args.accumulation_steps == 0 or processed_batches == iters
if should_step:
scaler.unscale_(optimizer)
if args.grad_clip > 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
scaler.step(optimizer)
scaler.update()
scheduler.step()
optimizer.zero_grad(set_to_none=True)
if args.debug_mode and is_main_process() and step % args.debug_interval == 0:
Logger(f"[OPD DEBUG] on_policy={on_policy}, prompt={prompt_texts[0]!r}")
Logger(f"[OPD DEBUG] completion={debug_completion!r}")
if step % args.log_interval == 0 or processed_batches == iters:
values = {
"loss/gkd": distributed_mean(gkd_loss),
"loss/aux": distributed_mean(aux_loss),
"loss/total": distributed_mean(total_loss),
"distill/forward_kl": distributed_mean(gkd_metrics["forward_kl"]),
"distill/reverse_kl": distributed_mean(gkd_metrics["reverse_kl"]),
"distill/top1_agreement": distributed_mean(gkd_metrics["top1_agreement"]),
"distill/valid_tokens": distributed_mean(gkd_metrics["valid_tokens"]),
"distill/on_policy": float(on_policy),
"distill/on_policy_fraction": on_policy_batches / processed_batches,
"rollout/avg_completion_length": distributed_mean(avg_completion_length),
"train/learning_rate": optimizer.param_groups[0]["lr"],
}
Logger(
f"Epoch:[{epoch + 1}/{args.epochs}]({step}/{start_step + iters}), "
f"GKD Loss: {values['loss/gkd']:.4f}, "
f"Forward KL: {values['distill/forward_kl']:.4f}, "
f"Reverse KL: {values['distill/reverse_kl']:.4f}, "
f"Top1 Agree: {values['distill/top1_agreement']:.4f}, "
f"On Policy: {int(on_policy)}, LR: {values['train/learning_rate']:.8f}"
)
if wandb and is_main_process():
wandb.log(values, step=epoch * iters + step)
if should_step and (step % args.save_interval == 0 or processed_batches == iters):
save_student(lm_config, epoch, step, wandb)
del input_ids, attention_mask, completion_mask
del student_output, student_logits, teacher_logits
del gkd_loss, gkd_metrics, aux_loss, total_loss
if on_policy:
del prompt_inputs, completion_ids, prompt_mask, rollout_result
else:
del labels
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="MiniMind Generalized/On-Policy Knowledge Distillation"
)
parser.add_argument("--data_path", type=str, default="dataset/rlaif.jsonl")
parser.add_argument("--student_model", type=str, default="full_sft")
parser.add_argument("--teacher_model", type=str, default="full_sft")
parser.add_argument("--tokenizer_path", type=str, default="model")
parser.add_argument("--save_dir", type=str, default="out")
parser.add_argument("--checkpoint_dir", type=str, default="checkpoints")
parser.add_argument("--save_weight", type=str, default="opd")
parser.add_argument("--epochs", type=int, default=1)
parser.add_argument("--batch_size", type=int, default=2)
parser.add_argument("--learning_rate", type=float, default=1e-6)
parser.add_argument("--device", type=str, default="auto")
parser.add_argument(
"--dtype", type=str, default="bfloat16",
choices=["float32", "float16", "bfloat16"]
)
parser.add_argument("--num_workers", type=int, default=4)
parser.add_argument("--accumulation_steps", type=int, default=1)
parser.add_argument("--grad_clip", type=float, default=1.0)
parser.add_argument("--log_interval", type=int, default=1)
parser.add_argument("--save_interval", type=int, default=100)
parser.add_argument("--max_seq_len", type=int, default=768)
parser.add_argument("--max_gen_len", type=int, default=256)
parser.add_argument("--max_train_steps", type=int, default=0)
parser.add_argument(
"--lmbda", type=float, default=1.0,
help="fraction of student-generated batches; 1.0 is pure OPD"
)
parser.add_argument(
"--beta", type=float, default=0.5,
help="generalized JSD coefficient; 0=forward KL, 1=reverse KL"
)
parser.add_argument("--temperature", type=float, default=1.0)
parser.add_argument("--distill_temperature", type=float, default=1.0)
parser.add_argument("--rollout_top_p", type=float, default=1.0)
parser.add_argument("--rollout_top_k", type=int, default=0)
parser.add_argument("--num_generations", type=int, default=1)
parser.add_argument("--student_hidden_size", type=int, default=768)
parser.add_argument("--student_num_layers", type=int, default=8)
parser.add_argument("--student_use_moe", type=int, default=0, choices=[0, 1])
parser.add_argument("--teacher_hidden_size", type=int, default=768)
parser.add_argument("--teacher_num_layers", type=int, default=8)
parser.add_argument("--teacher_use_moe", type=int, default=1, choices=[0, 1])
parser.add_argument("--from_resume", type=int, default=0, choices=[0, 1])
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--use_wandb", action="store_true")
parser.add_argument("--wandb_project", type=str, default="MiniMind-OPD")
parser.add_argument("--wandb_run_name", type=str, default="")
parser.add_argument(
"--wandb_mode", type=str, default="cloud",
choices=["cloud", "local", "disabled"]
)
parser.add_argument("--wandb_logdir", type=str, default="swanlog")
parser.add_argument("--use_compile", type=int, default=0, choices=[0, 1])
parser.add_argument("--debug_mode", action="store_true")
parser.add_argument("--debug_interval", type=int, default=20)
args = parser.parse_args()
validate_args(args)
local_rank = init_distributed_mode()
if dist.is_initialized():
args.device = f"cuda:{local_rank}"
else:
args.device = resolve_device(args.device)
rank = dist.get_rank() if dist.is_initialized() else 0
setup_seed(args.seed + rank)
resolve_path(args.save_dir).mkdir(parents=True, exist_ok=True)
resolve_path(args.checkpoint_dir).mkdir(parents=True, exist_ok=True)
max_position_embeddings = max(32768, args.max_seq_len + args.max_gen_len)
student_config = MiniMindConfig(
hidden_size=args.student_hidden_size,
num_hidden_layers=args.student_num_layers,
use_moe=bool(args.student_use_moe),
max_position_embeddings=max_position_embeddings,
)
teacher_config = MiniMindConfig(
hidden_size=args.teacher_hidden_size,
num_hidden_layers=args.teacher_num_layers,
use_moe=bool(args.teacher_use_moe),
max_position_embeddings=max_position_embeddings,
)
if student_config.vocab_size != teacher_config.vocab_size:
raise ValueError("GKD requires student and teacher to share the same vocabulary")
tokenizer = load_tokenizer(args.tokenizer_path, student_config.vocab_size)
device_type = args.device.split(":", 1)[0]
mixed_dtype = {
"float32": torch.float32,
"float16": torch.float16,
"bfloat16": torch.bfloat16,
}[args.dtype]
if device_type == "mps" and mixed_dtype == torch.bfloat16:
Logger("[OPD WARNING] MPS uses float16 for the frozen teacher")
mixed_dtype = torch.float16
teacher_dtype = mixed_dtype if device_type in {"cuda", "mps"} else torch.float32
autocast_ctx = (
torch.cuda.amp.autocast(dtype=mixed_dtype)
if device_type == "cuda" and mixed_dtype != torch.float32
else nullcontext()
)
model = load_native_model(
student_config, args.student_model, args.device, "student"
)
teacher_model = load_native_model(
teacher_config,
args.teacher_model,
args.device,
"teacher",
model_dtype=teacher_dtype,
).eval().requires_grad_(False)
Logger(f"Student params: {sum(p.numel() for p in model.parameters()) / 1e6:.3f}M")
Logger(f"Teacher params: {sum(p.numel() for p in teacher_model.parameters()) / 1e6:.3f}M")
checkpoint = (
lm_checkpoint(
student_config,
weight=args.save_weight,
save_dir=str(resolve_path(args.checkpoint_dir)),
)
if args.from_resume == 1 else None
)
train_ds = OPDDataset(resolve_path(args.data_path))
train_sampler = (
DistributedSampler(train_ds, shuffle=True, seed=args.seed)
if dist.is_initialized() else None
)
count_loader = DataLoader(
train_ds,
batch_size=args.batch_size,
sampler=train_sampler,
collate_fn=collate_conversations,
)
full_iters = len(count_loader)
iters = min(full_iters, args.max_train_steps) if args.max_train_steps > 0 else full_iters
optimizer_steps_per_epoch = math.ceil(iters / args.accumulation_steps)
total_optimizer_steps = max(1, optimizer_steps_per_epoch * args.epochs)
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
scheduler = CosineAnnealingLR(
optimizer, T_max=total_optimizer_steps, eta_min=args.learning_rate / 10
)
scaler = torch.cuda.amp.GradScaler(
enabled=(device_type == "cuda" and mixed_dtype == torch.float16)
)
start_epoch, start_step = 0, 0
if checkpoint:
model.load_state_dict(checkpoint["model"], strict=True)
optimizer.load_state_dict(checkpoint["optimizer"])
if "scheduler" in checkpoint:
scheduler.load_state_dict(checkpoint["scheduler"])
if "scaler" in checkpoint:
scaler.load_state_dict(checkpoint["scaler"])
start_epoch = checkpoint["epoch"]
start_step = checkpoint.get("step", 0)
Logger(f"Resuming GKD from epoch={start_epoch}, step={start_step}")
if args.use_compile == 1:
model = torch.compile(model)
Logger("torch.compile enabled")
if dist.is_initialized():
model = DistributedDataParallel(model, device_ids=[local_rank])
rollout_engine = create_rollout_engine(
engine_type="torch",
policy_model=model,
tokenizer=tokenizer,
device=args.device,
autocast_ctx=autocast_ctx,
)
wandb = None
if args.use_wandb and is_main_process():
import swanlab as wandb
wandb_id = checkpoint.get("wandb_id") if checkpoint else None
wandb.init(
project=args.wandb_project,
name=args.wandb_run_name or (
f"GKD-L{args.lmbda}-B{args.beta}-BS{args.batch_size}x"
f"{args.accumulation_steps}-LR{args.learning_rate}"
),
config=vars(args),
mode=args.wandb_mode,
logdir=str(resolve_path(args.wandb_logdir)),
id=wandb_id if args.wandb_mode == "cloud" else None,
resume="must" if wandb_id and args.wandb_mode == "cloud" else None,
)
optimizer.zero_grad(set_to_none=True)
for epoch in range(start_epoch, args.epochs):
if train_sampler:
train_sampler.set_epoch(epoch)
setup_seed(args.seed + epoch + rank)
indices = torch.randperm(len(train_ds)).tolist()
skip = start_step if epoch == start_epoch and start_step > 0 else 0
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
loader = DataLoader(
train_ds,
batch_sampler=batch_sampler,
num_workers=args.num_workers,
pin_memory=(device_type == "cuda"),
collate_fn=collate_conversations,
)
if skip > 0:
Logger(f"Epoch [{epoch + 1}/{args.epochs}]: skipping {skip} completed batches")
gkd_train_epoch(
epoch,
loader,
iters,
rollout_engine,
teacher_model,
student_config,
start_step=skip,
wandb=wandb,
)
start_step = 0
if wandb and is_main_process() and hasattr(wandb, "finish"):
wandb.finish()
if dist.is_initialized():
dist.barrier()
dist.destroy_process_group()

View File

@ -98,6 +98,7 @@ if __name__ == "__main__":
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量") parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
parser.add_argument('--max_seq_len', default=340, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)") parser.add_argument('--max_seq_len', default=340, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)") parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
parser.add_argument('--seed', default=42, type=int, help="随机种子(DDP下每个rank为seed+rank,每轮为seed+epoch)")
parser.add_argument("--data_path", type=str, default="../dataset/pretrain_t2t_mini.jsonl", help="预训练数据路径") parser.add_argument("--data_path", type=str, default="../dataset/pretrain_t2t_mini.jsonl", help="预训练数据路径")
parser.add_argument('--from_weight', default='none', type=str, help="基于哪个权重训练,为none则从头开始") parser.add_argument('--from_weight', default='none', type=str, help="基于哪个权重训练,为none则从头开始")
parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)") parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")
@ -109,7 +110,7 @@ if __name__ == "__main__":
# ========== 1. 初始化环境和随机种子 ========== # ========== 1. 初始化环境和随机种子 ==========
local_rank = init_distributed_mode() local_rank = init_distributed_mode()
if dist.is_initialized(): args.device = f"cuda:{local_rank}" if dist.is_initialized(): args.device = f"cuda:{local_rank}"
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0)) setup_seed(args.seed + (dist.get_rank() if dist.is_initialized() else 0))
# ========== 2. 配置目录、模型参数、检查ckp ========== # ========== 2. 配置目录、模型参数、检查ckp ==========
os.makedirs(args.save_dir, exist_ok=True) os.makedirs(args.save_dir, exist_ok=True)
@ -156,7 +157,7 @@ if __name__ == "__main__":
# ========== 8. 开始训练 ========== # ========== 8. 开始训练 ==========
for epoch in range(start_epoch, args.epochs): for epoch in range(start_epoch, args.epochs):
train_sampler and train_sampler.set_epoch(epoch) train_sampler and train_sampler.set_epoch(epoch)
setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist() setup_seed(args.seed + epoch); indices = torch.randperm(len(train_ds)).tolist()
skip = start_step if (epoch == start_epoch and start_step > 0) else 0 skip = start_step if (epoch == start_epoch and start_step > 0) else 0
batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip) batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)
loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True) loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)