CoolFace
Modelpublic

cwenzi/neuroflow-cpp

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
1likes
train_distill.py407 linesDownload Raw Back to scripts
1#!/usr/bin/env python3 -u2"""3NeuroFlow 蒸馏训练 — 用教师生成的数据训练学生模型4 5流程:6  1. 加载教师数据 (JSONL: {"prompt":..., "completion":...} 或纯文本)7  2. 加载 NeuroFlow 学生模型 (NF + LM head)8  3. 用 Cross-Entropy 训练学生预测教师文本 (滑动窗口多token预测)9  4. 保存 checkpoint (LMH1/LMH2 兼容格式)10 11用法:12  python3 -u scripts/train_distill.py \13    --teacher-data teacher_data.jsonl \14    --nf-model checkpoint/model.nfv1 \15    --lm-model checkpoint/lm_head.nfv1 \16    --tokenizer configs/tokenizer_128k.json \17    --output ./distill_output \18    --epochs 5 --lr 5e-6 --batch-size 32 \19    --train-nf   # 可选: 同时训练NF权重20 21注意: 必须使用 python3 -u 运行,或在后台运行时设置 PYTHONUNBUFFERED=122"""23 24import os25os.environ['PYTHONUNBUFFERED'] = '1'26 27import argparse, json, struct, sys, time, math28import numpy as np29 30sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))31from infer_full import load_nfv1, load_lmh1, load_tokenizer, encode, layernorm, gelu, softmax32 33 34def forward_with_cache(token_ids, nf_w, lm_w, vocab_size=128000):35    d_model = nf_w['input_proj.weight'].shape[1]36    hidden_dim = nf_w['input_proj.weight'].shape[0]37    cache = {}38 39    x = np.zeros(d_model, dtype=np.float32)40    copy_len = min(len(token_ids), d_model)41    for j in range(copy_len):42        x[j] = float(token_ids[j]) / float(vocab_size)43 44    h = nf_w['input_proj.weight'] @ x + nf_w['input_proj.bias']45    cache['input_proj.x'] = x.copy()46    cache['input_proj.pre_norm'] = h.copy()47    h = layernorm(h, nf_w['input_proj_norm.weight'], nf_w['input_proj_norm.bias'])48    cache['input_proj.post_norm'] = h.copy()49    h = gelu(h)50    cache['input_proj.post_gelu'] = h.copy()51 52    g1 = gelu(nf_w['sn.gate1.weight'] @ h + nf_w['sn.gate1.bias'])53    gates = softmax(nf_w['sn.gate2.weight'] @ g1 + nf_w['sn.gate2.bias'])54    cache['gates'] = gates.copy()55 56    h_ecn = h.copy()57    cache['ecn.h0'] = h_ecn.copy()58    for i in range(12):59        h_ecn = gelu(nf_w[f'ecn.dlpfc{i}.weight'] @ h_ecn + nf_w[f'ecn.dlpfc{i}.bias'])60    cache['ecn.last'] = h_ecn.copy()61 62    vmpfc = gelu(nf_w['ecn.vmpfc1.weight'] @ h_ecn + nf_w['ecn.vmpfc1.bias'])63    decision = nf_w['ecn.vmpfc2.weight'] @ vmpfc + nf_w['ecn.vmpfc2.bias']64    cache['decision'] = decision.copy()65 66    mem_encoded = nf_w['memory.encode.weight'] @ h + nf_w['memory.encode.bias']67    cache['mem_encoded'] = mem_encoded.copy()68 69    dmn_enc = gelu(nf_w['dmn.mem_encoder1.weight'] @ mem_encoded + nf_w['dmn.mem_encoder1.bias'])70    dmn_latent = nf_w['dmn.mem_encoder2.weight'] @ dmn_enc + nf_w['dmn.mem_encoder2.bias']71    cache['dmn_latent'] = dmn_latent.copy()72 73    assoc_outs = []74    for i in range(8):75        a1 = gelu(nf_w[f'dmn.head{i}.1.weight'] @ dmn_latent + nf_w[f'dmn.head{i}.1.bias'])76        a2 = nf_w[f'dmn.head{i}.2.weight'] @ a1 + nf_w[f'dmn.head{i}.2.bias']77        assoc_outs.append(a2)78    dmn_vision = gelu(nf_w['dmn.future_proj1.weight'] @ np.concatenate(assoc_outs) + nf_w['dmn.future_proj1.bias'])79    cache['dmn_vision'] = dmn_vision.copy()80 81    mem_bank = nf_w['memory.bank']82    att = softmax(mem_encoded @ mem_bank.T)83    retrieved = att @ mem_bank84    mem_retrieved = nf_w['memory.retrieve.weight'] @ retrieved + nf_w['memory.retrieve.bias']85    cache['mem_retrieved'] = mem_retrieved.copy()86 87    ecn_w = decision * gates[0]88    dmn_w = dmn_vision * gates[1]89    dmn_w_pad = np.zeros(hidden_dim, dtype=np.float32)90    dmn_w_pad[:dmn_w.shape[0]] = dmn_w91    mem_w = np.zeros(hidden_dim, dtype=np.float32)92    mem_w[:mem_retrieved.shape[0]] = mem_retrieved93    combined = np.concatenate([ecn_w, dmn_w_pad, mem_w])94    cache['combined'] = combined.copy()95 96    fused = nf_w['output_fusion.down.weight'] @ combined + nf_w['output_fusion.down.bias']97    fused_pre_relu = layernorm(fused, nf_w['output_fusion.bn_norm.weight'], nf_w['output_fusion.bn_norm.bias'])98    cache['fusion.pre_relu'] = fused_pre_relu.copy()99    fused_relu = np.maximum(0, fused_pre_relu)100    cache['fusion.post_relu'] = fused_relu.copy()101    nf_output = nf_w['output_fusion.up.weight'] @ fused_relu + nf_w['output_fusion.up.bias']102    nf_output = layernorm(nf_output, nf_w['output_fusion.norm.weight'], nf_w['output_fusion.norm.bias'])103    cache['nf_output'] = nf_output.copy()104 105    bridge_h = lm_w['bridge.weight'] @ nf_output + lm_w['bridge.bias']106    cache['bridge_h'] = bridge_h.copy()107 108    projected = lm_w['w_proj.weight'] @ bridge_h + lm_w['w_proj.bias']109    cache['projected'] = projected.copy()110 111    logits = lm_w['w_embed'] @ projected112    cache['logits'] = logits.copy()113 114    return logits, cache115 116 117 118def distill_step(token_ids, nf_w, lm_w, lr, vocab_size=128000, grad_clip=4.0,119                 train_nf=False, max_predictions=0):120    """单步蒸馏训练: 滑动窗口多token预测 + 反向传播121 122    对序列中每个位置 t,用 token_ids[:t+1] 前向预测 token_ids[t+1]。123    NF模型是"序列→单向量"架构,无法像Transformer那样单次前向获取所有位置hidden states,124    因此每个位置需要独立前向。max_predictions 限制每样本预测位置数以控制性能开销。125 126    --train-nf 反向传播路径止于 output_fusion.up/down,未穿过 ECN/DMN/Memory/SN gate。127    设计为渐进式解冻: 先训练 LM head + bridge,再解冻 output_fusion,最后解冻更深层。128    """129    seq_len = len(token_ids)130    if seq_len < 2:131        return 0.0132 133    total_loss = 0.0134    accum_lm_grads = {}135    accum_nf_grads = {}136    num_preds = 0137 138    positions = list(range(seq_len - 1))139    if max_predictions > 0 and len(positions) > max_predictions:140        step = max(1, len(positions) // max_predictions)141        positions = positions[::step][:max_predictions]142 143    for t in positions:144        prefix = token_ids[:t + 1]145        target_id = token_ids[t + 1]146        if target_id >= vocab_size:147            continue148 149        logits, cache = forward_with_cache(prefix, nf_w, lm_w, vocab_size)150 151        max_val = logits.max()152        exp_vals = np.exp(logits - max_val)153        sum_exp = exp_vals.sum()154        probs = exp_vals / sum_exp155 156        p_target = max(probs[target_id], 1e-10)157        total_loss += -math.log(p_target)158        num_preds += 1159 160        grad_logits = probs.copy()161        grad_logits[target_id] -= 1.0162 163        grad_w_embed = np.outer(grad_logits, cache['projected'])164        grad_projected = lm_w['w_embed'].T @ grad_logits165 166        grad_w_proj_weight = np.outer(grad_projected, cache['bridge_h'])167        grad_w_proj_bias = grad_projected.copy()168        grad_bridge_h = lm_w['w_proj.weight'].T @ grad_projected169 170        grad_bridge_weight = np.outer(grad_bridge_h, cache['nf_output'])171        grad_bridge_bias = grad_bridge_h.copy()172 173        step_lm_grads = {174            'w_embed': grad_w_embed,175            'w_proj.weight': grad_w_proj_weight,176            'w_proj.bias': grad_w_proj_bias,177            'bridge.weight': grad_bridge_weight,178            'bridge.bias': grad_bridge_bias,179        }180 181        for name, grad in step_lm_grads.items():182            if name not in accum_lm_grads:183                accum_lm_grads[name] = np.zeros_like(lm_w[name])184            accum_lm_grads[name] += grad185 186        if train_nf:187            grad_nf_output = lm_w['bridge.weight'].T @ grad_bridge_h188            grad_fused_relu = nf_w['output_fusion.up.weight'].T @ grad_nf_output189            grad_fused_pre_relu = grad_fused_relu * (cache['fusion.pre_relu'] > 0).astype(np.float32)190 191            step_nf_grads = {192                'output_fusion.up.weight': np.outer(grad_nf_output, cache['fusion.post_relu']),193                'output_fusion.up.bias': grad_nf_output.copy(),194                'output_fusion.down.weight': np.outer(grad_fused_pre_relu, cache['combined']),195                'output_fusion.down.bias': grad_fused_pre_relu.copy(),196            }197 198            for name, grad in step_nf_grads.items():199                if name in nf_w and nf_w[name].shape == grad.shape:200                    if name not in accum_nf_grads:201                        accum_nf_grads[name] = np.zeros_like(nf_w[name])202                    accum_nf_grads[name] += grad203 204    if num_preds == 0:205        return 0.0206 207    total_loss /= num_preds208 209    all_grads = {}210    for name, grad in accum_lm_grads.items():211        all_grads[f'lm.{name}'] = grad / num_preds212    for name, grad in accum_nf_grads.items():213        all_grads[f'nf.{name}'] = grad / num_preds214 215    total_norm = 0.0216    for g in all_grads.values():217        total_norm += np.sum(g ** 2)218    total_norm = math.sqrt(total_norm)219 220    clip_scale = 1.0221    if total_norm > grad_clip and grad_clip > 0:222        clip_scale = grad_clip / total_norm223 224    effective_lr = lr * clip_scale225    for name, grad in all_grads.items():226        if name.startswith('lm.'):227            key = name[3:]228            if key in lm_w and lm_w[key].shape == grad.shape:229                lm_w[key] -= effective_lr * grad230        elif name.startswith('nf.'):231            key = name[3:]232            if key in nf_w and nf_w[key].shape == grad.shape:233                nf_w[key] -= effective_lr * grad234 235    return total_loss236 237 238def save_lmh1(path, lm_w):239    with open(path, 'wb') as f:240        f.write(b'LMH1')241        for name, arr in lm_w.items():242            name_bytes = name.encode('utf-8')243            f.write(struct.pack('<I', len(name_bytes)))244            f.write(name_bytes)245            f.write(struct.pack('<I', len(arr.shape)))246            for d in arr.shape:247                f.write(struct.pack('<I', d))248            data = arr.astype(np.float32).tobytes()249            f.write(struct.pack('<I', len(data)))250            f.write(data)251        f.write(struct.pack('<I', 0))252 253 254def save_nfv1(path, nf_w):255    with open(path, 'wb') as f:256        f.write(b'NFv1')257        for name, arr in nf_w.items():258            name_bytes = name.encode('utf-8')259            f.write(struct.pack('<I', len(name_bytes)))260            f.write(name_bytes)261            f.write(struct.pack('<I', len(arr.shape)))262            for d in arr.shape:263                f.write(struct.pack('<I', d))264            data = arr.astype(np.float32).tobytes()265            f.write(struct.pack('<I', len(data)))266            f.write(data)267        f.write(struct.pack('<I', 0))268 269 270def main():271    parser = argparse.ArgumentParser(description='NeuroFlow Distillation Training')272    parser.add_argument('--teacher-data', required=True, help='教师数据 (JSONL/TXT)')273    parser.add_argument('--nf-model', required=True, help='学生 NF 模型路径')274    parser.add_argument('--lm-model', required=True, help='学生 LM head 路径')275    parser.add_argument('--tokenizer', required=True, help='分词器路径')276    parser.add_argument('--output', default='./distill_output', help='输出目录')277    parser.add_argument('--epochs', type=int, default=5)278    parser.add_argument('--lr', type=float, default=5e-6)279    parser.add_argument('--batch-size', type=int, default=32)280    parser.add_argument('--save-interval', type=int, default=500)281    parser.add_argument('--grad-clip', type=float, default=4.0)282    parser.add_argument('--train-nf', action='store_true', help='同时训练NF权重(默认只训练LM head)')283    parser.add_argument('--resume', default='', help='断点续训: 指定checkpoint目录')284    parser.add_argument('--max-predictions', type=int, default=8, help='每样本最大预测位置数(0=全部, 默认8)')285    args = parser.parse_args()286 287    os.makedirs(args.output, exist_ok=True)288 289    print("加载教师数据...")290    samples = []291    with open(args.teacher_data, 'r', encoding='utf-8') as f:292        for line in f:293            line = line.strip()294            if not line:295                continue296            try:297                rec = json.loads(line)298                text = rec.get('prompt', '') + rec.get('completion', '')299            except json.JSONDecodeError:300                text = line301            if len(text) >= 10:302                samples.append(text)303    print(f"   {len(samples)} 个样本")304 305    print("加载分词器...")306    vocab, id2token, merge_ranks = load_tokenizer(args.tokenizer)307    print(f"   词表: {len(vocab)} tokens")308 309    print("分词...")310    tokenized = []311    total_tokens = 0312    t0 = time.time()313    progress_interval = max(1, len(samples) // 20)314    for si, text in enumerate(samples):315        ids = encode(text, vocab, merge_ranks, max_len=128)316        if len(ids) >= 4:317            tokenized.append(ids)318            total_tokens += len(ids)319        if (si + 1) % progress_interval == 0 or si == len(samples) - 1:320            elapsed = time.time() - t0321            pct = (si + 1) * 100 // len(samples)322            rate = (si + 1) / max(elapsed, 0.01)323            eta = (len(samples) - si - 1) / max(rate, 0.01)324            print(f"   分词进度: {si+1}/{len(samples)} ({pct}%) | "325                  f"{rate:.0f} samples/s | ETA {eta:.0f}s | "326                  f"tokens={total_tokens:,}")327    avg_len = total_tokens / max(len(tokenized), 1)328    elapsed = time.time() - t0329    print(f"   完成: {total_tokens:,} tokens ({avg_len:.0f} avg/sample) | 耗时 {elapsed:.1f}s")330 331    print("加载学生模型...")332    nf_w = load_nfv1(args.nf_model)333    lm_w = load_lmh1(args.lm_model)334    d_model = nf_w['input_proj.weight'].shape[1]335    hidden_dim = nf_w['input_proj.weight'].shape[0]336    print(f"   NF: {len(nf_w)}层 | LM: {len(lm_w)}层 | d_model={d_model} hidden={hidden_dim}")337 338    start_step = 0339    start_epoch = 0340    if args.resume:341        print(f"断点续训: {args.resume}")342        nf_w = load_nfv1(f"{args.resume}/model.nfv1")343        lm_w = load_lmh1(f"{args.resume}/lm_head.nfv1")344        state_path = f"{args.resume}/training_state.json"345        if os.path.exists(state_path):346            with open(state_path) as sf:347                state = json.load(sf)348            start_step = state.get('step', 0)349            start_epoch = state.get('epoch', 1) - 1350            print(f"   恢复: step={start_step}, epoch={start_epoch+1}")351 352    mode_str = "LM+NF" if args.train_nf else "LM only"353    print(f"\n开始蒸馏训练 ({args.epochs} epochs, lr={args.lr}, batch={args.batch_size}, "354          f"grad_clip={args.grad_clip}, mode={mode_str})")355    print(f"   每样本预测位置数: {args.max_predictions if args.max_predictions > 0 else int(avg_len)} (max_predictions={args.max_predictions})")356    print("=" * 60)357 358    global_step = start_step359    for epoch in range(start_epoch, args.epochs):360        epoch_loss = 0.0361        steps = 0362 363        indices = list(range(len(tokenized)))364        np.random.shuffle(indices)365 366        for i in range(0, len(indices), args.batch_size):367            batch_indices = indices[i:i + args.batch_size]368 369            batch_loss = 0.0370            for idx in batch_indices:371                loss = distill_step(tokenized[idx], nf_w, lm_w, args.lr,372                                    vocab_size=len(vocab), grad_clip=args.grad_clip,373                                    train_nf=args.train_nf, max_predictions=args.max_predictions)374                batch_loss += loss375 376            batch_loss /= len(batch_indices)377            epoch_loss += batch_loss378            steps += 1379            global_step += 1380 381            if steps % 10 == 0:382                print(f"  [Epoch {epoch+1}][Step {global_step}] loss={batch_loss:.4f}")383 384            if args.save_interval > 0 and global_step % args.save_interval == 0:385                ckpt_dir = f"{args.output}/step_{global_step}"386                os.makedirs(ckpt_dir, exist_ok=True)387                save_nfv1(f"{ckpt_dir}/model.nfv1", nf_w)388                save_lmh1(f"{ckpt_dir}/lm_head.nfv1", lm_w)389                state = {"step": global_step, "epoch": epoch + 1, "loss": batch_loss, "lr": args.lr}390                with open(f"{ckpt_dir}/training_state.json", 'w') as sf:391                    json.dump(state, sf, indent=2)392                print(f"  Checkpoint: {ckpt_dir}")393 394        avg_loss = epoch_loss / max(steps, 1)395        print(f"=== Epoch {epoch+1} 完成, avg_loss={avg_loss:.4f} ===\n")396 397    final_dir = f"{args.output}/final"398    os.makedirs(final_dir, exist_ok=True)399    save_nfv1(f"{final_dir}/model.nfv1", nf_w)400    save_lmh1(f"{final_dir}/lm_head.nfv1", lm_w)401    print(f"最终模型已保存: {final_dir}")402    print("蒸馏训练完成")403 404 405if __name__ == '__main__':406    main()407