F 发布的文章

项目概述

本文档记录了在 RTX 5090 GPU 环境下搭建 MedicalGPT 预训练环境并成功完成训练的完整过程。项目基于 Qwen2.5-0.5B 模型,使用 LoRA (PEFT) 方法进行预训练。

环境信息

  • 操作系统: Linux 5.15.0-94-generic
  • GPU: NVIDIA GeForce RTX 5090
  • Python: 3.11.5 (从 3.8.10 升级)
  • PyTorch: 2.9.0.dev20250805+cu128 (nightly 版本)
  • CUDA: 12.8

初始环境检查

首先检查了当前环境的基本信息:

python --version
# Python 3.8.10

nvidia-smi
# NVIDIA GeForce RTX 5090

发现 Python 版本较旧,且需要配置网络代理来访问 Hugging Face 等资源。

网络代理配置

由于网络环境限制,需要配置 Clash 代理来访问外部资源。

代理文件准备

  • clash-linux-amd64-n2023-09-05-gdcc8d87.gz - Clash 可执行文件
  • 性价比机场.yaml - 代理配置文件
  • Country.mmdb - GeoIP 数据库

代理服务启动

# 解压并设置权限
gunzip clash-linux-amd64-n2023-09-05-gdcc8d87.gz
chmod +x clash-linux-amd64-n2023-09-05-gdcc8d87

# 创建配置目录
mkdir -p ~/.config/clash

# 复制配置文件
cp "性价比机场.yaml" ~/.config/clash/config.yaml
cp Country.mmdb ~/.config/clash/
cp clash-linux-amd64-n2023-09-05-gdcc8d87 ~/.config/clash/clash

# 启动代理服务
cd ~/.config/clash && ./clash -d . &

代理测试

# 测试代理连接
curl -x http://127.0.0.1:7890 http://httpbin.org/ip
curl -x http://127.0.0.1:7890 https://huggingface.co

代理配置成功,可以正常访问外部资源。

依赖环境升级

Python 版本升级

由于 PyTorch nightly 版本需要更新的 Python 版本,将 Python 从 3.8.10 升级到 3.11.5:

conda install python=3.11 -y

PyTorch 安装

根据 NVIDIA 官方建议,安装支持 RTX 5090 的 PyTorch nightly 版本:

pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu128

核心依赖安装

安装项目所需的核心依赖:

pip install transformers accelerate peft datasets loguru scikit-learn tensorboard

遇到的问题与解决方案

问题 1: CUDA 兼容性错误

错误信息

RuntimeError: CUDA error: no kernel image is available for execution on the device

原因分析
PyTorch 2.4.1 虽然支持 CUDA 12.1,但不完全兼容 RTX 5090 的 sm_120 架构。

解决方案

  1. 升级 Python 到 3.11 版本
  2. 安装 PyTorch nightly 版本 (2.9.0.dev20250805+cu128)
  3. 使用 CUDA 12.8 支持

验证结果

import torch
print(f'PyTorch version: {torch.__version__}')  # 2.9.0.dev20250805+cu128
print(f'CUDA available: {torch.cuda.is_available()}')  # True
print(f'Device name: {torch.cuda.get_device_name()}')  # NVIDIA GeForce RTX 5090

问题 2: PEFT 导入错误

错误信息

ImportError: cannot import name 'prepare_model_for_kbit_training' from 'peft'

原因分析
peft 0.3.0 版本中没有 prepare_model_for_kbit_training 函数,该函数在较新版本中才有。

解决方案
升级 peft 库到最新版本:

pip install --upgrade peft

验证结果

from peft import prepare_model_for_kbit_training
print('PEFT import successful')

问题 3: Transformers 兼容性问题

错误信息

ImportError: cannot import name 'is_torch_tpu_available' from 'transformers'

原因分析
transformers 4.55.0 版本中移除了 is_torch_tpu_available 函数。

解决方案
修改 pretraining.py 文件,移除对 is_torch_tpu_available 的依赖:

# 移除导入
from transformers import (
    AutoConfig,
    AutoModelForCausalLM,
    AutoTokenizer,
    HfArgumentParser,
    Trainer,
    Seq2SeqTrainingArguments,
    # is_torch_tpu_available,  # 移除这行
    set_seed,
    BitsAndBytesConfig,
)

# 修改使用位置
compute_metrics=compute_metrics if training_args.do_eval else None,
preprocess_logits_for_metrics=preprocess_logits_for_metrics
if training_args.do_eval
else None,

问题 4: 缺失依赖

错误信息

ModuleNotFoundError: No module named 'loguru'
RuntimeError: TensorBoardCallback requires tensorboard to be installed

解决方案
安装缺失的依赖:

pip install loguru scikit-learn tensorboard

训练配置

模型配置

  • 基础模型: Qwen/Qwen2.5-0.5B
  • 训练方法: LoRA (PEFT)
  • LoRA 配置:

    • rank: 8
    • alpha: 16.0
    • dropout: 0.05
    • target_modules: ['down_proj', 'gate_proj', 'k_proj', 'o_proj', 'q_proj', 'up_proj', 'v_proj']

训练参数

  • 批次大小: 4 (per_device_train_batch_size)
  • 梯度累积: 8 (gradient_accumulation_steps)
  • 学习率: 2e-4
  • 训练轮数: 0.5 epochs
  • 最大样本数: 10000 (训练), 10 (评估)
  • 块大小: 512 tokens
  • 优化器: AdamW (fused)
  • 精度: bfloat16

数据配置

  • 训练数据: 3 个文本文件

    • en_article_tail500.txt
    • fever.txt
    • tianlongbabu.txt
  • 数据预处理: 10 个并行工作进程
  • 训练样本数: 621 个
  • 评估样本数: 10 个

训练执行

启动训练

cd MedicalGPT
bash run_pt.sh

训练过程监控

训练过程中观察到以下关键指标:

  1. 模型加载成功

    • 成功加载 Qwen2.5-0.5B 模型
    • LoRA 配置应用成功
    • 可训练参数:4,399,104 (0.88% 的总参数)
  2. 数据处理

    • 原始数据集:3,876 个样本
    • 分词处理:使用 10 个并行进程
    • 分块处理:512 tokens 块大小
  3. 训练进度

    • 训练步数:10 步
    • 训练时间:14.67 秒
    • 训练速度:21.152 samples/second

训练结果

性能指标

  • 训练损失: 3.4131
  • 评估损失: 3.0955
  • 评估准确率: 41.66%
  • 困惑度: 22.10
  • 训练效率: 0.681 steps/second

模型输出

训练完成后,模型保存在 outputs-pt-qwen-v1/ 目录中,包含:

  • 模型检查点文件
  • LoRA 权重文件
  • 训练配置文件
  • TensorBoard 日志文件

验证结果

# 检查输出目录
ls -la outputs-pt-qwen-v1/
# 包含 adapter_config.json, adapter_model.safetensors 等文件

经验总结

关键技术点

  1. GPU 兼容性:新架构 GPU 需要使用对应的 PyTorch nightly 版本
  2. 依赖管理:及时升级关键依赖库,注意版本兼容性
  3. 网络配置:在受限网络环境下,合理配置代理服务
  4. 错误调试:逐步排查依赖和兼容性问题

最佳实践

  1. 环境隔离:使用 conda 管理 Python 环境
  2. 版本控制:记录所有依赖的版本信息
  3. 渐进式调试:从基础功能开始,逐步添加复杂功能
  4. 日志记录:详细记录每个步骤和遇到的问题

性能优化

  1. 数据预处理:使用多进程并行处理
  2. 内存管理:合理设置批次大小和梯度累积
  3. 精度选择:使用 bfloat16 平衡精度和性能
  4. 模型优化:使用 LoRA 减少可训练参数

后续工作

  1. 模型评估:在更多数据集上评估模型性能
  2. 超参数调优:优化学习率、批次大小等参数
  3. 模型部署:将训练好的模型部署到生产环境
  4. 持续训练:使用更多数据进行增量训练

参考资料


本文档记录了从环境搭建到模型训练完成的完整过程,可作为类似项目的参考指南。

在 LLM Agent 训练中,有时存在需要通过代码行号进行补全的方法。

这个脚本给任意给定的代码行统一添加代码行号。

import json
import re
import argparse

def add_line_numbers_to_input(input_jsonl_path, output_jsonl_path):
    with open(input_jsonl_path, 'r', encoding='utf-8') as fin, \
         open(output_jsonl_path, 'w', encoding='utf-8') as fout:
        
        for line in fin:
            try:
                data = json.loads(line)
                if "input" in data:
                    data["input"] = process_code_block(data["input"])
                fout.write(json.dumps(data, ensure_ascii=False) + '\n')
            except json.JSONDecodeError:
                print(f"JSON 解码错误: {line}")

def process_code_block(code_str):
    # 匹配代码块(包含```c和```, 如果需要匹配其他语言可以之后更改)
    code_block_match = re.search(r'(```c\n)(.*?)(\n```)', code_str, re.DOTALL)

    if not code_block_match:
        print("未找到符合条件的代码块")
        return code_str
    
    # 获取代码块各部分
    start_marker = code_block_match.group(1)  # ```c\n
    code_content = code_block_match.group(2)   # 实际代码内容
    end_marker = code_block_match.group(3)    # \n```

    # 处理行号(保留原始缩进)
    code_lines = code_content.split('\n')
    number_lines = []
    line_num = 1

    for code_line in code_lines:
        number_lines.append(f"{line_num}: {code_line}")
        line_num += 1
    
    # 重新建立代码块
    processed_code = (
        code_str[:code_block_match.start()] +  # 保留代码块前的所有内容
        start_marker +                        # ```c\n
        '\n'.join(number_lines) +           # 带行号的代码
        end_marker +                          # \n```
        code_str[code_block_match.end():]     # 保留代码块后的所有内容
    )

    return processed_code

def main():
    parser = argparse.ArgumentParser(description="Add line numbers to code blocks in a JSONL file.")
    parser.add_argument('-i', '--input', required=True, help='Input JSONL file path')
    parser.add_argument('-o', '--output', required=False, default='output.jsonl', help='Output JSONL filepath, default=output.jsonl')
    args = parser.parse_args()

    add_line_numbers_to_input(args.input, args.output)
    print(f"Processed file saved to {args.output}")


if __name__  == '__main__':
    main()

和狗子出来玩
偶遇一群跑出来吃草的小动物
不知道是🪿还是🦆

但非常塔诺西🥹

03fd3cecedef40d2cc43ae8ec6a656.jpg

525d0909972d0229dcb9ca34f32999.jpg

ps.
长沙夏天就是太热了。我俩跑到烈士公园的麦当劳坐到下午六点才敢出去...
说好如果有时间的话冬天再来一次

pps.
好想去水族馆..///

在修复代码 bug 的 Agent check_list 策略中,一个基本的三步方法如下:

  1. LLM 阅读给定代码块,根据给定的参考错误列表找到于 bug 描述相对应的有问题的“代码行号”
  2. 根据有问题的“代码片段”,判断代码片段是否确实违反代码规范,以 0(正确)和 1(错误)表示。
  3. 对于错误值为 1 的代码片段,进行 bug 修复。

很明显可以看到在阶段 1 和阶段 2 之间需要运行某一个脚本,来根据“代码行号”反向爬取代码块中的“代码片段”。

这么做的原因是在阶段 1 直接让模型输出“代码片段”的策略可能存在大量错误,因为模型的评估标准较为宽松,并不能保证准确无误地找到确切的代码片段。

以下脚本可以完成给定数据的处理工作。

此外,为了方便模型微调,还对于有问题的代码片段增加了一个随机的 snippet_id,使用 uuid.uuid4().hex 方法生成。

import json
import re
import uuid

with open("config/config.json", encoding="utf-8")as conf:
    con = json.loads(conf.read())

def extract_code_snippets_from_jsonl(input_jsonl, output_file):
    # 从代码行号提取出代码片段区间,批量处理
    line_count = 0
    with open(input_jsonl, 'r', encoding='utf-8') as f_in:
        with open(output_file, 'w', encoding='utf-8') as f_out:
            for line in f_in:
                line = line.strip()
                if not line:
                    continue

                try:
                    data = json.loads(line)
                    
                    output_rules_str = data["output"]
                    json_match = re.search(r'```json\s*\n(.*?)```', output_rules_str, re.DOTALL)
                    all_rules = json.loads(json_match.group(1) if json_match else json.loads(output_rules_str))

                    code_block = data["input"]
                    code_match = re.search(r'```c\s*\n(.*?)```', code_block, re.DOTALL)
                    if not code_match:
                        continue

                    raw_code_lines = code_match.group(1).split('\n')
                    
                    line_rules = []

                    for rule in all_rules:
                        if not rule.get("output"):
                            continue
                            
                        code_snippets = []
                        
                        for code_range in rule["output"]:
                            start = code_range["start_line"] - 1
                            end = code_range.get("end_line", code_range["start_line"]) - 1
                            
                            if 0 <= start < len(raw_code_lines) and 0 <= end < len(raw_code_lines):
                                snippet_lines = []
                                for line in raw_code_lines[start:end+1]:
                                    cleaned_line = re.sub(r'^\s*\d+[: ]\s*', '', line)
                                    snippet_lines.append(cleaned_line.rstrip())
                                
                                snippet_text = '\n'.join(snippet_lines).strip()
                                
                                if snippet_text:
                                    code_snippets.append({
                                        "snippet_id": uuid.uuid4().hex,
                                        "content": snippet_text
                                    })
                        
                        if code_snippets:
                            line_rules.append({
                                "rule": rule["rule"],
                                "code": code_snippets
                            })
                            line_count += 1
                            
                    if line_rules:
                        line_rules_str = json.dumps(line_rules, ensure_ascii=False)
                        output_data = {
                            "output": line_rules_str
                        }
                        # 将当前行的所有规则写入一行(不换行)
                        f_out.write(json.dumps(output_data, ensure_ascii=False) + '\n')
                        
                except json.JSONDecodeError:
                    continue  # 跳过无效的 JSON 行
    print(f"Processed {line_count} rules")

工作中有清洗 jsonl 文件的需求,原因是 LLM 输出的内容有可能存在错误的补全,不能直接全部用于 Fine-tuning。
这个脚本清洗了 jsonl 文件中 input/output == ""input 行重复 的情况。

import json
import random

input_file = 'file.jsonl'
output_file = 'file_cleaned.jsonl'

# 用于存储唯一 input 的行
input_map = {}

total_lines = 0          # 总有效行数(合法 JSON)
filtered_count = 0       # 被踢掉的行数(output 为空或 input 为空)

with open(input_file, 'r', encoding='utf-8') as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        try:
            item = json.loads(line)
            total_lines += 1

            # 检查 output 是否为空
            if item.get("output", "").strip() == "":
                filtered_count += 1
                continue

            inp = item.get("input")
            if inp is None:
                filtered_count += 1
                continue

            # 对重复 input 随机选择一条保留
            if inp not in input_map:
                input_map[inp] = [item]
            else:
                input_map[inp].append(item)
        except json.JSONDecodeError:
            continue  # 跳过无效 JSON 行

# 随机保留每组重复 input 中的一条
unique_items = [random.choice(items) for items in input_map.values()]

# 写入输出文件
with open(output_file, 'w', encoding='utf-8') as f:
    for item in unique_items:
        f.write(json.dumps(item, ensure_ascii=False) + '\n')

# 最终统计
retained_count = len(unique_items)
print(f"总处理条数(有效 JSON): {total_lines}")
print(f"被踢掉的条数(空 output 或无 input): {filtered_count}")
print(f"最终保留条数(按 input 去重): {retained_count}")
print(f"文件被保存为 {output_file}")