Buckets:
| 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<size_t> prompt_ids = tokenizer.encode(prompt, max_seq_len); | |
| std::vector<size_t> 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<size_t>& target_ids, | |
| const std::vector<float>& 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<float>(output.valid_token_count); | |
| float inv_n = 1.0f / static_cast<float>(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<CausalLMHead>(lm_config); | |
| if (!cfg.ckpt_path.empty()) { | |
| load_lm_checkpoint(*model_, cfg.ckpt_path); | |
| } | |
| tokenizer_ = std::make_unique<BPETokenizer>(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<AdamW>(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<CosineScheduler>(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<float>( | |
| std::chrono::duration<double>(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<float>(step_count) : 0.0f; | |
| auto epoch_end = std::chrono::steady_clock::now(); | |
| float epoch_elapsed = static_cast<float>( | |
| std::chrono::duration<double>(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<size_t> 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<float>(valid_count) : 0.0f; | |
| } | |
| } // namespace neuroflow |
Xet Storage Details
- Size:
- 12.3 kB
- Xet hash:
- c23cc8d8973fff94d36ca10267db2abf166f08604236afcacb6392405e69c6ef
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.