#include "neuroflow/sft.hpp" #include #include #include #include #include #include #include #include #include namespace neuroflow { SFTDataLoader::SFTDataLoader(const std::string& jsonl_path, size_t max_samples) { std::ifstream ifs(jsonl_path); if (!ifs) { std::cerr << "SFT数据文件无法打开: " << jsonl_path << std::endl; return; } std::string line; while (std::getline(ifs, line)) { if (line.empty() || line[0] == '#') continue; std::string instruction = extract_json_string(line, "instruction"); std::string response = extract_json_string(line, "response"); unescape_json(instruction); unescape_json(response); if (instruction.empty() || response.empty()) { invalid_count_++; continue; } samples_.push_back({instruction, response}); if (max_samples > 0 && samples_.size() >= max_samples) break; } std::cerr << "SFT数据加载: " << samples_.size() << " 样本, " << invalid_count_ << " 无效" << std::endl; } bool SFTDataLoader::has_next() const { return cursor_ < samples_.size(); } SFTSample SFTDataLoader::next() { return samples_[cursor_++]; } void SFTDataLoader::reset() { cursor_ = 0; } void SFTDataLoader::shuffle(std::mt19937& rng) { std::shuffle(samples_.begin(), samples_.end(), rng); } SFTTrainingTensors build_sft_training_tensors(const SFTSample& sample, BPETokenizer& tokenizer, size_t max_seq_len) { SFTTrainingTensors result; std::string prompt = sample.instruction + "\n"; std::string full_text = prompt + sample.response; std::vector prompt_ids = tokenizer.encode(prompt, max_seq_len); std::vector full_ids = tokenizer.encode(full_text, max_seq_len); if (full_ids.size() > max_seq_len) { full_ids.resize(max_seq_len); } if (full_ids.size() < 2) { result.instruction_len = 0; return result; } result.input_ids.assign(full_ids.begin(), full_ids.end() - 1); result.target_ids.assign(full_ids.begin() + 1, full_ids.end()); result.instruction_len = std::min(prompt_ids.size(), result.input_ids.size()); result.loss_mask.resize(result.target_ids.size(), 0.0f); for (size_t i = result.instruction_len; i < result.target_ids.size(); ++i) { result.loss_mask[i] = 1.0f; } return result; } MaskedCEOutput masked_cross_entropy(const Tensor& logits, size_t vocab_size, const std::vector& target_ids, const std::vector& loss_mask) { MaskedCEOutput output; output.loss = 0.0f; output.valid_token_count = 0; size_t seq_len = target_ids.size(); if (seq_len == 0 || logits.numel() == 0) { output.logits_grad = Tensor({1, vocab_size}, QuantType::FP32); memset(output.logits_grad.as_fp32(), 0, output.logits_grad.data_size_); return output; } output.logits_grad = Tensor({seq_len, vocab_size}, QuantType::FP32); memset(output.logits_grad.as_fp32(), 0, output.logits_grad.data_size_); float* lg = output.logits_grad.as_fp32(); for (size_t t = 0; t < seq_len; ++t) { if (loss_mask[t] < 0.5f) continue; size_t target = target_ids[t]; if (target >= vocab_size) target = 1; const float* pred = logits.as_fp32() + t * vocab_size; float max_val = -1e30f; for (size_t j = 0; j < vocab_size; ++j) { if (pred[j] > max_val) max_val = pred[j]; } float sum_exp = 0.0f; for (size_t j = 0; j < vocab_size; ++j) { sum_exp += std::exp(pred[j] - max_val); } float log_sum_exp = max_val + std::log(sum_exp); float token_loss = -(pred[target] - log_sum_exp); if (!std::isfinite(token_loss)) continue; output.loss += token_loss; output.valid_token_count++; float* grad_row = lg + t * vocab_size; for (size_t j = 0; j < vocab_size; ++j) { float softmax_val = std::exp(pred[j] - max_val) / sum_exp; grad_row[j] = softmax_val; if (j == target) grad_row[j] -= 1.0f; } } if (output.valid_token_count > 0) { output.loss /= static_cast(output.valid_token_count); float inv_n = 1.0f / static_cast(output.valid_token_count); for (size_t i = 0; i < seq_len * vocab_size; ++i) { lg[i] *= inv_n; } } return output; } SFTTrainer::SFTTrainer(const SFTTrainConfig& cfg) : config(cfg) { CausalLMConfig lm_config; lm_config.vocab_size = 128000; lm_config.d_model = 512; lm_config.max_seq_len = cfg.max_seq_len; lm_config.num_attn_layers = 4; lm_config.num_attn_heads = 8; lm_config.n_kv_heads = 2; lm_config.use_rope = true; lm_config.use_qk_norm = true; lm_config.use_swiglu = true; lm_config.use_bridge = true; lm_config.weight_tying = true; lm_config.pooling = "last"; model_ = std::make_unique(lm_config); if (!cfg.ckpt_path.empty()) { load_lm_checkpoint(*model_, cfg.ckpt_path); } tokenizer_ = std::make_unique(cfg.tokenizer_path); size_t total_steps = 0; { SFTDataLoader tmp_loader(cfg.data_path); total_steps = tmp_loader.total_samples() * cfg.epochs; } optimizer_ = std::make_unique(cfg.learning_rate, cfg.adam_beta1, cfg.adam_beta2, cfg.adam_eps, cfg.weight_decay); model_->register_trainable_params(*optimizer_, cfg.learning_rate, cfg.weight_decay); scheduler_ = std::make_unique(cfg.learning_rate, total_steps, 0.1f, cfg.warmup_ratio); } void SFTTrainer::train() { SFTDataLoader loader(config.data_path); if (loader.total_samples() == 0) { std::cerr << "SFT训练: 无有效样本" << std::endl; return; } std::cerr << "SFT训练开始: " << loader.total_samples() << " 样本, " << config.epochs << " epochs" << std::endl; model_->train(); size_t global_step = 0; auto train_start = std::chrono::steady_clock::now(); for (int epoch = 0; epoch < config.epochs; ++epoch) { auto epoch_start = std::chrono::steady_clock::now(); loader.reset(); std::mt19937 shuffle_rng(config.seed + epoch); loader.shuffle(shuffle_rng); float epoch_loss = 0.0f; size_t step_count = 0; while (loader.has_next()) { SFTSample sample = loader.next(); float lr = scheduler_->get_lr(global_step); optimizer_->set_lr(lr); float sample_loss = train_on_sample(sample); global_step++; if (std::isfinite(sample_loss)) { epoch_loss += sample_loss; step_count++; } if (config.log_interval > 0 && global_step % config.log_interval == 0) { auto now = std::chrono::steady_clock::now(); float elapsed = static_cast( std::chrono::duration(now - train_start).count()); std::cerr << "[SFT] step=" << global_step << " epoch=" << (epoch + 1) << " loss=" << sample_loss << " lr=" << optimizer_->get_lr() << " elapsed=" << elapsed << "s" << std::endl; } if (config.save_interval > 0 && global_step % config.save_interval == 0) { std::string cdir = config.output_dir + "/checkpoint_step" + std::to_string(global_step); std::filesystem::create_directories(cdir); save_lm_checkpoint(*model_, cdir + "/lm_head.nfv1"); std::cerr << "[SFT] Checkpoint: step=" << global_step << std::endl; } } float avg_loss = (step_count > 0) ? epoch_loss / static_cast(step_count) : 0.0f; auto epoch_end = std::chrono::steady_clock::now(); float epoch_elapsed = static_cast( std::chrono::duration(epoch_end - epoch_start).count()); std::cerr << "[SFT] Epoch " << (epoch + 1) << "/" << config.epochs << " avg_loss=" << avg_loss << " elapsed=" << epoch_elapsed << "s" << std::endl; std::string cdir = config.output_dir + "/checkpoint_epoch" + std::to_string(epoch + 1); std::filesystem::create_directories(cdir); save_lm_checkpoint(*model_, cdir + "/lm_head.nfv1"); } std::filesystem::create_directories(config.output_dir); save_lm_checkpoint(*model_, config.output_dir + "/lm_head_sft_final.nfv1"); std::cerr << "SFT训练完成, 模型已保存: " << config.output_dir << "/lm_head_sft_final.nfv1" << std::endl; } float SFTTrainer::train_on_sample(const SFTSample& sample) { SFTTrainingTensors tensors = build_sft_training_tensors(sample, *tokenizer_, config.max_seq_len); if (tensors.input_ids.empty() || tensors.instruction_len == 0) { return 0.0f; } size_t seq_len = tensors.input_ids.size(); size_t vocab_size = model_->config_.vocab_size; float total_loss = 0.0f; float total_grad_norm = 0.0f; for (size_t t = 0; t < seq_len; ++t) { std::vector input_prefix(tensors.input_ids.begin(), tensors.input_ids.begin() + t + 1); Tensor logits = model_->forward_for_training(input_prefix); size_t target_id = tensors.target_ids[t]; if (target_id >= vocab_size) target_id = 1; const float* pred = logits.as_fp32(); float max_val = -1e30f; for (size_t j = 0; j < vocab_size; ++j) { if (pred[j] > max_val) max_val = pred[j]; } float sum_exp = 0.0f; for (size_t j = 0; j < vocab_size; ++j) { sum_exp += std::exp(pred[j] - max_val); } float log_sum_exp = max_val + std::log(sum_exp); float token_loss = -(pred[target_id] - log_sum_exp); if (tensors.loss_mask[t] < 0.5f) { continue; } if (!std::isfinite(token_loss)) { std::cerr << "[SFT WARN] NaN/Inf loss at token " << t << ", skipping" << std::endl; continue; } Tensor logits_grad({1, vocab_size}, QuantType::FP32); float* lg = logits_grad.as_fp32(); float grad_norm = 0.0f; for (size_t j = 0; j < vocab_size; ++j) { float softmax_val = std::exp(pred[j] - max_val) / sum_exp; lg[j] = softmax_val; if (j == target_id) lg[j] -= 1.0f; grad_norm += lg[j] * lg[j]; } float gn = std::sqrt(grad_norm); float clip_scale = 1.0f; if (!std::isfinite(gn) || (gn > config.grad_clip && config.grad_clip > 0.0f)) { clip_scale = config.grad_clip / gn; } if (clip_scale < 1.0f) { float* lg2 = logits_grad.as_fp32(); for (size_t j = 0; j < vocab_size; ++j) lg2[j] *= clip_scale; } auto lm_grads = model_->backward_from_logits(logits_grad); model_->assign_grads_to_optimizer(*optimizer_, lm_grads); optimizer_->step(); total_loss += token_loss; total_grad_norm += grad_norm; } size_t valid_count = 0; for (size_t i = 0; i < tensors.loss_mask.size(); ++i) { if (tensors.loss_mask[i] > 0.5f) valid_count++; } return (valid_count > 0) ? total_loss / static_cast(valid_count) : 0.0f; } } // namespace neuroflow