BACK_TO_BASE
AI 模型:LLM / VLM / VLA 指南
Engineering Notebook // Build Log
/
20:50:19
/
NOTEBOOK_ENTRY

AI 模型:LLM / VLM / VLA 指南

目录 1. 硬件环境与底层框架 The Bedrock 1 硬件环境与底层框架 the bedrock 2. 数据工程:模型的"燃料" Data Pipeline 2 数据工程模型的燃料 data pipeline 3. 模型架构与训练范式 Modeling 3 模型架构与训练范式 modeling 4. 针对性实战案例 Practical Use Cases 4 针对性实战案例 practical use cases 5. 性能优化与…

Notebook Time
9 min
Image Frames
1
View Tracks
106
学习
FIELD_GUIDE

FIELD GUIDE

Use the guide rail to jump between sections.

目录


1. 硬件环境与底层框架 (The Bedrock)

1.1 显存需求的第一性原理计算

在动手之前,你必须回答一个问题:我需要多少 GPU 显存?

1.1.1 参数存储

一个参数在不同精度下占用的字节数:

精度每参数字节数7B 模型参数占用13B70B
FP324 bytes28 GB52 GB280 GB
FP16 / BF162 bytes14 GB26 GB140 GB
INT81 byte7 GB13 GB70 GB
INT40.5 byte3.5 GB6.5 GB35 GB

公式显存_参数 = 参数量 × 每参数字节数

1.1.2 训练时的总显存估算(全精度 Mixed Precision)

训练时,显存远不只参数本身。以 AdamW + Mixed Precision (FP16/BF16 + FP32 master weights) 为例:

总显存 ≈ 模型参数 + 梯度 + 优化器状态 + 激活值

─────────────────────────────────────
合计(不含激活值): 16 bytes × P

→ 7B 模型: 16 × 7×10⁹ ≈ 112 GB(仅参数+优化器)
→ 13B 模型: 16 × 13×10⁹ ≈ 208 GB
→ 70B 模型: 16 × 70×10⁹ ≈ 1120 GB ≈ 1.1 TB

激活值(Activation Memory) 取决于 batch_size × seq_len × hidden_dim × num_layers,通常可通过 Gradient Checkpointing(梯度检查点) 将激活值显存降低到 O(L)O(\sqrt{L})LL 为层数),代价是约 33% 的额外计算时间。

经验法则(全参数训练):所需总显存 ≈ 18-20 × 参数量(以 bytes 计)

1.1.3 实际硬件对照表

GPU显存可全参数训练可 LoRA 微调(BF16)可 QLoRA 微调(4bit)
RTX 4070 (12GB)12 GB≤ 0.5B≤ 3B≤ 7B
RTX 4090 (24GB)24 GB≤ 1B≤ 7B≤ 13B
A100 (80GB)80 GB≤ 4B≤ 30B≤ 70B
8×A100 (640GB)640 GB≤ 30B≤ 70B+≤ 180B
H100 (80GB)80 GB≤ 5B≤ 34B≤ 70B

1.1.4 存储空间估算

别忘了磁盘:

  • 模型 checkpoint:一个 7B FP16 模型约 14 GB,训练中一般保留 3-5 个 checkpoint → 70 GB
  • 预训练语料:1T token 的文本语料(tokenized)约 2-4 TB
  • 多模态数据:LAION-400M 图像数据集约 80+ TB(原始图像)
  • 建议:NVMe SSD ≥ 2TB 用于高频 I/O,HDD 用于冷存储

1.2 本地化训练环境搭建

1.2.1 基础环境(以 Ubuntu 22.04 + CUDA 为例)

# 1. 安装 NVIDIA 驱动(推荐使用 .run 包或 apt 方式)
sudo apt update
sudo apt install -y nvidia-driver-545  # 根据显卡型号选择

# 验证
nvidia-smi

# 2. 安装 CUDA Toolkit(推荐 12.1+,与 PyTorch 版本对齐)
wget https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run
sudo sh cuda_12.4.0_550.54.14_linux.run --toolkit --silent

# 添加环境变量
echo 'export PATH=/usr/local/cuda-12.4/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc

# 验证
nvcc --version

# 3. 安装 cuDNN
# 从 https://developer.nvidia.com/cudnn 下载对应版本
sudo dpkg -i cudnn-local-repo-ubuntu2204-9.x.x_1.0-1_amd64.deb

# 4. 安装 NCCL(多卡通信库,多卡训练必备)
sudo apt install libnccl2 libnccl-dev

1.2.2 Python 训练栈

# 推荐使用 conda 管理环境
conda create -n train python=3.11 -y
conda activate train

# PyTorch(务必对齐 CUDA 版本)
pip install torch==2.3.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

# 核心训练框架
pip install transformers>=4.43.0       # Hugging Face 模型库
pip install datasets>=2.20.0           # 数据加载
pip install accelerate>=0.33.0         # 分布式训练抽象层
pip install deepspeed>=0.14.0          # ZeRO 优化器
pip install peft>=0.12.0              # LoRA/QLoRA
pip install trl>=0.9.0                # RLHF/DPO 训练
pip install bitsandbytes>=0.43.0      # 量化支持
pip install flash-attn --no-build-isolation  # FlashAttention-2

# 可选但推荐
pip install wandb                      # 实验追踪
pip install sentencepiece protobuf     # Tokenizer 依赖
pip install einops                     # 张量操作

1.2.3 关键框架的角色定位

┌─────────────────────────────────────────────────────────────────┐
│                      你的训练脚本                                │
│                   (train.py / train.yaml)                       │
├─────────────┬───────────────┬───────────────┬──────────────────┤
│ Transformers│     TRL       │     PEFT      │    Datasets      │
│  (模型定义)  │ (RLHF/DPO)   │ (LoRA/QLoRA)  │   (数据加载)      │
├─────────────┴───────────────┴───────────────┴──────────────────┤
│                      Accelerate                                 │
│           (统一的分布式训练接口 / 设备分配)                        │
├─────────────────────────────────────────────────────────────────┤
│              DeepSpeed (ZeRO-1/2/3)                             │
│       (优化器分片 / 梯度分片 / 参数分片 / Offload)                │
├─────────────────────────────────────────────────────────────────┤
│       PyTorch (FSDP / DDP)  +  NCCL (多卡通信)                  │
├─────────────────────────────────────────────────────────────────┤
│              CUDA  +  cuDNN  +  FlashAttention                  │
└─────────────────────────────────────────────────────────────────┘

各框架解释

  • Accelerate:Hugging Face 出品,一层薄抽象。你写单卡代码,它帮你处理多卡、多机、混合精度。配置驱动,不侵入代码。
  • DeepSpeed:微软出品,核心是 ZeRO(Zero Redundancy Optimizer)系列优化。
  • FSDP:PyTorch 原生的全分片数据并行(Fully Sharded Data Parallel),与 DeepSpeed ZeRO-3 理念相似。

1.2.4 Accelerate 配置示例

# 交互式配置
accelerate config

# 或直接编写 YAML(推荐)
# accelerate_config.yaml(单机双卡 + DeepSpeed ZeRO-2 示例)
compute_environment: LOCAL_MACHINE
distributed_type: DEEPSPEED
deepspeed_config:
  deepspeed_config_file: ds_config.json
  zero3_init_flag: false
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 2            # GPU 数量
gpu_ids: 0,1

1.3 消费级显卡的分布式策略

如果你只有 2-4 张 RTX 4070/4090,以下策略能让你训练更大的模型:

1.3.1 DeepSpeed ZeRO 三阶段对比

                    显存占用(示例: 7B 模型, 2 卡)
                    ┌──────────────────────────────┐
ZeRO-0 (DDP)       │ 每卡存完整副本: ~112 GB/卡    │  ← 放不下
                    ├──────────────────────────────┤
ZeRO-1             │ 优化器状态分片: ~80 GB/卡      │  ← 仍然放不下
(Optimizer Split)   ├──────────────────────────────┤
ZeRO-2             │ +梯度分片: ~66 GB/卡           │  ← 还是大
(+Gradient Split)   ├──────────────────────────────┤
ZeRO-3             │ +参数分片: ~28 GB/卡           │  ← 勉强可行
(+Parameter Split)  ├──────────────────────────────┤
ZeRO-3 + Offload   │ 优化器状态→CPU: ~16 GB/卡     │  ← 消费级可行
                    └──────────────────────────────┘

1.3.2 DeepSpeed ZeRO-3 + CPU Offload 配置

{
  "bf16": { "enabled": true },
  "zero_optimization": {
    "stage": 3,
    "offload_optimizer": {
      "device": "cpu",
      "pin_memory": true
    },
    "offload_param": {
      "device": "cpu",
      "pin_memory": true
    },
    "overlap_comm": true,
    "contiguous_gradients": true,
    "sub_group_size": 1e9,
    "reduce_bucket_size": "auto",
    "stage3_prefetch_bucket_size": "auto",
    "stage3_param_persistence_threshold": "auto",
    "stage3_max_live_parameters": 1e9,
    "stage3_max_reuse_distance": 1e9,
    "stage3_gather_16bit_weights_on_model_save": true
  },
  "gradient_accumulation_steps": 16,
  "gradient_clipping": 1.0,
  "steps_per_print": 10,
  "train_batch_size": "auto",
  "train_micro_batch_size_per_gpu": 1,
  "wall_clock_breakdown": false
}

关键参数解读

  • offload_optimizer.device: "cpu":将 Adam 的 m/v 状态放到 CPU 内存,极大减少 GPU 显存
  • offload_param.device: "cpu":将不在当前前向/反向计算中的参数也放到 CPU(速度会更慢,但显存极省)
  • gradient_accumulation_steps: 16:模拟更大 batch size,弥补显存不足导致的小 micro-batch

1.3.3 FSDP vs DeepSpeed:如何选择?

维度FSDPDeepSpeed ZeRO
来源PyTorch 官方微软
集成度PyTorch 原生,无额外依赖需要安装 deepspeed 包
CPU Offload支持(但生态较新)成熟且稳定
NVMe Offload不支持支持(ZeRO-Infinity)
HF 集成Accelerate 原生支持Accelerate 原生支持
推荐场景2-8 卡同构集群消费级显卡 + CPU Offload

实际建议

  • 4090 × 2 训练 7B QLoRA → Accelerate + FSDP 足够
  • 4070 × 4 全参训练 7B → DeepSpeed ZeRO-3 + CPU Offload
  • 跨机器训练 → DeepSpeed 配合 pdsh 或 Slurm

2. 数据工程:模型的"燃料" (Data Pipeline)

"Garbage in, garbage out" 在大模型时代变为 "Data quality is all you need"

2.1 通用 LLM 预训练数据

2.1.1 数据来源全景

预训练语料(目标: 数百B ~ 数T tokens)
├── Web 爬取
│   ├── Common Crawl(最大公开爬取数据集,PB 级)
│   ├── FineWeb(Hugging Face 清洗后的 15T token 数据集)
│   └── RefinedWeb(Falcon 团队清洗的 5T tokens)
├── 书籍
│   ├── Project Gutenberg(公版图书)
│   └── Books3(争议数据集,注意版权)
├── 学术论文
│   ├── arXiv(LaTeX 源码)
│   ├── S2ORC(Semantic Scholar)
│   └── PubMed(生物医学)
├── 代码
│   ├── The Stack v2(Hugging Face, 许可证过滤)
│   └── StarCoder Data
├── 百科与结构化
│   ├── Wikipedia(多语言)
│   └── StackExchange
└── 领域特定
    ├── 法律: CaseText, 裁判文书网
    ├── 医学: PubMed, MIMIC
    └── 金融: SEC Filings, 财经新闻

2.1.2 数据清洗流水线(The Cleaning Pipeline)

这是最被低估但最重要的环节。以下是工业级流水线的完整步骤:

原始数据 (Raw HTML/Text)
    │
    ▼
┌─────────────────────┐
│ 1. 文本提取          │  trafilatura / resiliparse 从 HTML 提取正文
│    (Extraction)      │  去除导航栏、广告、页脚等 boilerplate
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ 2. 语言识别          │  fastText lid.176.bin → 过滤非目标语言
│    (Language ID)     │  阈值通常设 0.65+
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ 3. URL/域名过滤      │  黑名单: 成人站点, 垃圾农场, 版权风险域名
│    (URL Filtering)   │  UT1 黑名单 + 自定义规则
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ 4. 质量过滤          │  基于启发式规则:
│    (Quality Filter)  │  - 文档长度 > 50 words
│                      │  - 平均句子长度合理 (5-100 words)
│                      │  - 特殊字符/大写比例异常 → 过滤
│                      │  - 停用词比例检测(防机器翻译垃圾)
│                      │  - perplexity 过滤(KenLM 语言模型打分)
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ 5. 文档级去重        │  MinHash + LSH (Locality-Sensitive Hashing)
│    (Deduplication)   │  工具: datatrove / text-dedup
│                      │  阈值: Jaccard > 0.8 判定为重复
│                      │  ⚠ 这一步可以去掉 30-50% 的数据
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ 6. 段落/句子级去重   │  Exact substring dedup (suffix array)
│    (Fine Dedup)      │  去除在多文档中重复出现的样板段落
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ 7. PII 脱敏          │  正则 + NER 模型去除:
│    (PII Removal)     │  电话号码, 邮箱, 身份证号, 地址
└─────────┬───────────┘
          │
          ▼
  清洗后的干净语料 → Tokenization → 序列化

2.1.3 工业级工具:Datatrove

Datatrove 是 Hugging Face 开发的可扩展数据处理库,FineWeb 数据集就是用它构建的。

"""
Datatrove 流水线示例:处理 Common Crawl WARC 文件
"""
from datatrove.pipeline.readers import WarcReader
from datatrove.pipeline.filters import (
    URLFilter,
    LanguageFilter,
    GopherRepetitionFilter,
    GopherQualityFilter,
    C4QualityFilter,
)
from datatrove.pipeline.dedup import MinhashDedupSignature, MinhashDedupBuckets, MinhashDedupCluster, MinhashDedupFilter
from datatrove.pipeline.extractors import Trafilatura
from datatrove.pipeline.writers import JsonlWriter
from datatrove.executor import LocalPipelineExecutor

# 阶段 1: 提取 + 过滤
pipeline_1 = [
    WarcReader("s3://commoncrawl/crawl-data/CC-MAIN-2024-10/"),
    Trafilatura(),                      # HTML → 正文提取
    URLFilter(),                        # URL 黑名单过滤
    LanguageFilter(language="zh"),      # 只保留中文
    GopherQualityFilter(),              # Gopher 论文的质量规则
    GopherRepetitionFilter(),           # 重复内容过滤
    C4QualityFilter(),                  # C4 数据集的过滤规则
    JsonlWriter("output/filtered/"),
]

executor = LocalPipelineExecutor(
    pipeline=pipeline_1,
    tasks=100,          # 并行任务数
    workers=16,         # 进程数
)
executor.run()

# 阶段 2: MinHash 去重(需要多步)
# Step 2.1: 计算签名
# Step 2.2: LSH 分桶
# Step 2.3: 聚类
# Step 2.4: 去重过滤

规模参考:FineWeb 处理了 96 个 Common Crawl 快照,最终产出 15T tokens 的高质量英文数据。中文可参考 CCI (Chinese Corpus Internet) 的实践。

2.1.4 Tokenization 策略

Tokenizer 的选择直接影响模型效率。

Tokenizer代表模型词表大小中文效率
BPEGPT-4, LLaMA32K-128K取决于训练语料比例
SentencePiece (Unigram)T5, mBART32K-250K较好
WordPieceBERT30K一般

关键决策

  • 如果做中文模型,务必确保 tokenizer 训练语料中中文占比足够(否则一个汉字被拆成 3-4 个 token,效率极低)
  • LLaMA-3 的 tokenizer 词表扩大到 128K,中文效率显著提升
  • 自训练 tokenizer 示例:
from tokenizers import Tokenizer, models, trainers, pre_tokenizers

# 训练 BPE tokenizer
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)

trainer = trainers.BpeTrainer(
    vocab_size=64000,
    special_tokens=["<s>", "</s>", "<pad>", "<unk>", "<mask>"],
    min_frequency=2,
    show_progress=True,
)

# 在你的语料上训练
files = ["chinese_corpus.txt", "english_corpus.txt", "code_corpus.txt"]
tokenizer.train(files, trainer)
tokenizer.save("my_tokenizer.json")

# 验证中文效率
test = "大规模预训练语言模型的核心在于数据质量"
encoded = tokenizer.encode(test)
print(f"字符数: {len(test)}, Token数: {len(encoded.ids)}")
print(f"压缩率: {len(test)/len(encoded.ids):.2f} 字符/token")
# 理想情况: 中文应达到 1.5-2.5 字符/token

2.2 多模态 VLM 数据

2.2.1 图文对齐数据的类型

VLM 训练数据
├── 预训练阶段(图文对齐 / Feature Alignment)
│   ├── 弱标注图文对 (Weakly-labeled Image-Text Pairs)
│   │   ├── LAION-5B:  50亿图文对, 来自网页 alt-text
│   │   ├── COYO-700M: 7亿图文对
│   │   └── DataComp: 可复现的数据筛选基准
│   │
│   └── 说明: 这些数据质量参差不齐, 但量大
│
├── 指令微调阶段(Visual Instruction Tuning)
│   ├── LLaVA-Instruct-150K: GPT-4 生成的图像问答指令
│   ├── ShareGPT4V: 高质量图像描述
│   ├── ALLaVA: 多样化视觉指令
│   └── 说明: 质量 >> 数量, 通常 100K-1M 条
│
└── 评估数据
    ├── VQAv2, GQA:      视觉问答
    ├── TextVQA, OCRBench: OCR 相关
    ├── MMBench, MME:      综合多模态评测
    └── POPE:              幻觉检测

2.2.2 图文对数据的构建与合成

方法 1: 从网页爬取 + CLIP 过滤

"""
使用 CLIP Score 过滤低质量图文对
原理: CLIP 模型可以衡量图像和文本的语义相似度
"""
import torch
from transformers import CLIPProcessor, CLIPModel
from PIL import Image

model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")

def filter_image_text_pair(image_path: str, text: str, threshold: float = 0.25):
    """
    CLIP score > threshold → 保留
    DataComp 论文建议阈值 0.25-0.30
    """
    image = Image.open(image_path)
    inputs = processor(text=[text], images=image, return_tensors="pt", padding=True)

    with torch.no_grad():
        outputs = model(**inputs)
        # cosine similarity
        score = outputs.logits_per_image.item() / 100.0

    return score > threshold, score

方法 2: 用大模型合成高质量描述(LLaVA 方法)

"""
用 GPT-4V 或开源 VLM 为图像生成详细描述/问答对
这是 LLaVA 论文的核心数据构造方法
"""
import openai
import base64

def generate_visual_instruction(image_path: str) -> dict:
    with open(image_path, "rb") as f:
        image_b64 = base64.b64encode(f.read()).decode()

    # 生成三种类型的数据
    prompts = {
        "conversation": "基于这张图片,生成一段自然的多轮对话(3-5轮)。",
        "detail_description": "请详细描述这张图片中的所有内容,包括物体、位置关系、颜色、动作等。",
        "complex_reasoning": "基于这张图片,生成一个需要复杂推理才能回答的问题和答案。"
    }

    results = {}
    for task, prompt in prompts.items():
        response = openai.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
                ]
            }]
        )
        results[task] = response.choices[0].message.content

    return results

2.2.3 数据格式标准化

VLM 的训练数据最终需要统一为以下格式:

{
  "id": "000001",
  "image": "images/000001.jpg",
  "conversations": [
    {
      "from": "human",
      "value": "<image>\n请描述这张图片中的场景。"
    },
    {
      "from": "gpt",
      "value": "这张图片展示了一个阳光明媚的公园..."
    }
  ]
}

<image> token:这是一个特殊标记,在模型内部会被替换为视觉编码器输出的特征向量序列(通常 256-576 个 visual tokens)。

2.3 具身 VLA 数据

2.3.1 VLA 数据的独特性

VLA(Vision-Language-Action)数据不同于 LLM/VLM,它必须包含:

VLA 训练数据 = 视觉观测 + 语言指令 + 动作标签

每一条轨迹 (Trajectory) 的结构:
{
    "task_description": "pick up the red cup and place it on the table",
    "steps": [
        {
            "timestamp": 0.0,
            "observation": {
                "image": "frame_000.jpg",              # RGB 图像 (H×W×3)
                "depth": "depth_000.npy",               # 深度图 (可选)
                "proprio": [0.1, 0.2, 0.3, 0.0, ...]   # 本体感受 (关节角度/末端位姿)
            },
            "action": {
                "cartesian": [dx, dy, dz, dRx, dRy, dRz, gripper],  # 7-DOF 动作
                # 或者
                "joint": [dq1, dq2, dq3, dq4, dq5, dq6, gripper]   # 关节空间动作
            }
        },
        ...
    ]
}

2.3.2 动作数据的归一化

这是 VLA 数据工程中最容易出错的环节。 不同机械臂的动作空间差异巨大:

"""
VLA 动作数据归一化
关键: 不同机械臂的关节范围、末端执行器坐标系、控制频率都不同
必须统一归一化到 [-1, 1] 或 [0, 1]
"""
import numpy as np

class ActionNormalizer:
    """
    针对机械臂动作空间的归一化器
    支持两种模式:
    1. min-max 归一化 → [-1, 1]
    2. z-score 归一化 → μ=0, σ=1 (然后 clip)
    """

    def __init__(self, method="minmax"):
        self.method = method
        self.stats = {}

    def fit(self, actions: np.ndarray):
        """
        在整个数据集上计算统计量
        actions: shape (N, action_dim), 例如 (100000, 7)
        """
        if self.method == "minmax":
            self.stats["min"] = np.percentile(actions, 1, axis=0)   # 用 1% 分位数避免异常值
            self.stats["max"] = np.percentile(actions, 99, axis=0)
        elif self.method == "zscore":
            self.stats["mean"] = actions.mean(axis=0)
            self.stats["std"] = actions.std(axis=0) + 1e-8

    def normalize(self, action: np.ndarray) -> np.ndarray:
        if self.method == "minmax":
            normed = 2 * (action - self.stats["min"]) / (self.stats["max"] - self.stats["min"] + 1e-8) - 1
            return np.clip(normed, -1, 1)
        elif self.method == "zscore":
            normed = (action - self.stats["mean"]) / self.stats["std"]
            return np.clip(normed, -5, 5)  # clip 极端值

    def denormalize(self, normed_action: np.ndarray) -> np.ndarray:
        """推理时需要反归一化回真实动作空间"""
        if self.method == "minmax":
            return (normed_action + 1) / 2 * (self.stats["max"] - self.stats["min"]) + self.stats["min"]
        elif self.method == "zscore":
            return normed_action * self.stats["std"] + self.stats["mean"]


# 使用示例
normalizer = ActionNormalizer(method="minmax")

# 假设你有一个 Franka Panda 的数据集
# 动作维度: [x, y, z, rx, ry, rz, gripper] = 7-DOF
all_actions = np.load("franka_actions.npy")  # (N, 7)
normalizer.fit(all_actions)

# 归一化单条数据
raw_action = np.array([0.5, 0.3, 0.1, 0.0, 0.0, 0.1, 1.0])
normed = normalizer.normalize(raw_action)
print(f"归一化后: {normed}")  # 所有值在 [-1, 1]

2.3.3 动作的序列化:离散化 vs 连续化

VLA 模型将动作预测为 token,有两种方式:

方式 1: 离散化(OpenVLA / RT-2 的做法)

"""
将连续动作离散化为 token (bins)
例如: 将 [-1, 1] 均匀分为 256 个 bin → 每个动作维度对应一个 token ID
"""
def discretize_action(action: np.ndarray, num_bins: int = 256) -> list[int]:
    """
    action: 归一化后的动作向量, 值域 [-1, 1]
    返回: token ID 列表
    """
    # [-1, 1] → [0, num_bins-1]
    bin_indices = ((action + 1) / 2 * (num_bins - 1)).astype(int)
    bin_indices = np.clip(bin_indices, 0, num_bins - 1)
    return bin_indices.tolist()

def undiscretize_action(tokens: list[int], num_bins: int = 256) -> np.ndarray:
    """反离散化: token → 连续动作"""
    action = np.array(tokens) / (num_bins - 1) * 2 - 1
    return action

# 示例
action = np.array([0.5, -0.3, 0.0, 0.1, -0.5, 0.8, 1.0])  # 7-DOF
tokens = discretize_action(action, num_bins=256)
print(f"离散化 tokens: {tokens}")  # e.g., [191, 89, 128, 140, 64, 230, 255]

方式 2: 连续回归(直接预测浮点数)

# 在模型最后一层接一个 MLP 回归头
class ActionHead(torch.nn.Module):
    def __init__(self, hidden_dim, action_dim=7):
        super().__init__()
        self.mlp = torch.nn.Sequential(
            torch.nn.Linear(hidden_dim, 256),
            torch.nn.GELU(),
            torch.nn.Linear(256, action_dim),
            torch.nn.Tanh()  # 输出 [-1, 1]
        )

    def forward(self, hidden_states):
        # hidden_states: 最后一个 token 的隐状态
        return self.mlp(hidden_states)

2.3.4 开源机器人数据集

数据集规模机器人任务
Open X-Embodiment100万+ 轨迹22 种机器人527 种技能
DROID76K 轨迹Franka多样化操作
BridgeData V260K 轨迹WidowX桌面操作
RH20T110K 轨迹多种中国团队, 丰富场景

使用 Open X-Embodiment 的 RLDS 格式

import tensorflow_datasets as tfds
dataset = tfds.load("fractal20220817_data", split="train")
for episode in dataset:
    for step in episode["steps"]:
        image = step["observation"]["image"]
        action = step["action"]  # 已归一化

2.4 数据混合策略(Data Mixing)

预训练时不同数据源的混合比例对模型质量影响巨大:

Llama-3 数据混合(推测):
├── Web 文本:    82%     (FineWeb 等)
├── 代码:        8%      (The Stack)
├── 学术论文:    4%      (arXiv, PubMed)
├── 书籍:        4%      (公版)
└── 百科+问答:   2%      (Wikipedia, StackExchange)

中文模型建议配比:
├── 中文 Web:    40%
├── 英文 Web:    25%     (跨语言能力)
├── 代码:        15%
├── 中文百科:    8%
├── 学术论文:    7%
└── 中文书籍:    5%

Hugging Face 教程参考: FineWeb 博客 详细记录了如何从 96 个 CC 快照中清洗出 15T token 的完整过程,包括每个过滤步骤对下游基准测试的影响消融实验。强烈推荐阅读


3. 模型架构与训练范式 (Modeling)

3.1 训练阶段全景

一个完整的模型训练包含以下阶段(不是每个都必须):

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  Pre-training    │───▶│       SFT        │───▶│    DPO/RLHF     │───▶│   Deployment    │
│  (预训练)        │    │  (指令微调)       │    │   (人类对齐)     │    │    (部署)       │
│                  │    │                  │    │                  │    │                  │
│ 目标: 学语言知识  │    │ 目标: 学会遵从指令 │    │ 目标: 安全+有用   │    │ 量化+推理优化    │
│ 数据: TB级语料   │    │ 数据: 10K-1M指令对│    │ 数据: 偏好对数据  │    │                  │
│ 代价: $1M-$10M+ │    │ 代价: $100-$10K  │    │ 代价: $1K-$100K │    │                  │
│ 时间: 周-月      │    │ 时间: 小时-天     │    │ 时间: 小时-天    │    │                  │
└─────────────────┘    └─────────────────┘    └─────────────────┘    └─────────────────┘

        ↑ 大多数人从这里开始(使用已有的 base model)
              ↑ 大多数个人开发者从这里开始

3.1.1 Pre-training(预训练)

目标:让模型学会语言的统计规律、世界知识、推理能力

训练目标:Next Token Prediction(自回归)

Lpretrain=t=1TlogP(xtx1,x2,,xt1;θ)\mathcal{L}_{\text{pretrain}} = -\sum_{t=1}^{T} \log P(x_t | x_1, x_2, \ldots, x_{t-1}; \theta)

"""
预训练的核心循环(简化版)
注意: 真实预训练需要大量工程优化,这里展示核心逻辑
"""
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

# 预训练就是简单的语言建模
for batch in dataloader:
    input_ids = batch["input_ids"]        # (B, seq_len)
    labels = input_ids.clone()            # labels 就是 input 向左移一位

    outputs = model(input_ids=input_ids, labels=labels)
    loss = outputs.loss                   # CrossEntropyLoss

    loss.backward()
    optimizer.step()

关键超参数(预训练)

  • Learning Rate: 3e-4 → cosine decay 到 3e-5
  • Warmup: 前 2000 steps 线性 warmup
  • Batch Size: 4M tokens/batch(通过 gradient accumulation 实现)
  • Sequence Length: 2048 → 4096 → 8192(逐步增加)
  • Weight Decay: 0.1
  • Gradient Clipping: 1.0

3.1.2 SFT(Supervised Fine-Tuning / 指令微调)

目标:让 base model 学会遵从人类指令

数据格式

<|begin_of_text|><|start_header_id|>system<|end_header_id|>
你是一个有帮助的AI助手。<|eot_id|>
<|start_header_id|>user<|end_header_id|>
请解释什么是梯度下降<|eot_id|>
<|start_header_id|>assistant<|end_header_id|>
梯度下降是一种优化算法...<|eot_id|>

关键:只在 assistant 回复部分计算 loss

"""
SFT 的核心: 只对 response 部分计算 loss
"""
def create_sft_labels(input_ids, response_start_idx):
    labels = input_ids.clone()
    # 将 prompt 部分的 label 设为 -100 (忽略)
    labels[:, :response_start_idx] = -100
    return labels

# 使用 TRL 的 SFTTrainer 更方便:
from trl import SFTTrainer, SFTConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")
dataset = load_dataset("tatsu-lab/alpaca", split="train")

training_args = SFTConfig(
    output_dir="./sft_output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-5,           # 比预训练低一个数量级
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    bf16=True,
    logging_steps=10,
    save_strategy="epoch",
    max_seq_length=2048,
)

trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
)
trainer.train()

3.1.3 DPO(Direct Preference Optimization)

目标:让模型的输出更符合人类偏好(更安全、更有用)

RLHF vs DPO

  • RLHF:先训练奖励模型(Reward Model) → 再用 PPO 算法优化策略模型 → 复杂且不稳定
  • DPO:直接用偏好数据优化模型 → 简单且有效(2023 年后主流方法)

LDPO=E[logσ(βlogπθ(ywx)πref(ywx)βlogπθ(ylx)πref(ylx))]\mathcal{L}_{\text{DPO}} = -\mathbb{E} \left[ \log \sigma \left( \beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)} \right) \right]

其中 ywy_w 是偏好的(chosen)回答,yly_l 是不偏好的(rejected)回答。

"""
DPO 训练示例
数据格式: 每条数据包含 prompt + chosen response + rejected response
"""
from trl import DPOTrainer, DPOConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset

# 加载 SFT 后的模型作为起点
model = AutoModelForCausalLM.from_pretrained("./sft_output")
ref_model = AutoModelForCausalLM.from_pretrained("./sft_output")  # 冻结的参考模型
tokenizer = AutoTokenizer.from_pretrained("./sft_output")

# DPO 数据集格式
# {"prompt": "...", "chosen": "好的回答", "rejected": "差的回答"}
dataset = load_dataset("argilla/ultrafeedback-binarized-preferences", split="train")

training_args = DPOConfig(
    output_dir="./dpo_output",
    num_train_epochs=1,               # DPO 通常只需 1 epoch
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=5e-7,               # 非常小的学习率!
    beta=0.1,                         # DPO 温度参数 (越大越保守)
    bf16=True,
    max_length=1024,
    max_prompt_length=512,
)

trainer = DPOTrainer(
    model=model,
    ref_model=ref_model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
)
trainer.train()

3.2 LoRA 与 QLoRA:参数高效微调的核心

为什么需要 LoRA? 全参数微调 7B 模型需要 ~112 GB 显存,而 LoRA 只需 ~16 GB。

3.2.1 LoRA 的数学原理

核心思想:预训练模型的权重更新矩阵 ΔW\Delta W 是**低秩(Low-Rank)**的。

对于原始权重矩阵 W0Rd×kW_0 \in \mathbb{R}^{d \times k},LoRA 将更新分解为:

W=W0+ΔW=W0+BAW = W_0 + \Delta W = W_0 + BA

其中:

  • BRd×rB \in \mathbb{R}^{d \times r}ARr×kA \in \mathbb{R}^{r \times k}
  • rmin(d,k)r \ll \min(d, k)(秩远小于原始维度)
  • W0W_0 被冻结,只训练 AABB
原始 W_q: 4096 × 4096 = 16,777,216 参数
LoRA (r=16): 4096×16 + 16×4096 = 131,072 参数
→ 仅原始的 0.78%!

全模型:
→ 仅总参数的 0.26%

初始化

  • AA 用高斯随机初始化
  • BB 初始化为零矩阵
  • 训练开始时 ΔW=BA=0\Delta W = BA = 0,即模型从预训练权重出发

前向传播

# 伪代码
def lora_forward(x, W_0, A, B, alpha, r):
    """
    x:     输入 (batch, seq_len, d)
    W_0:   冻结的预训练权重 (d, k) — 不参与梯度计算
    A:     LoRA 下投影矩阵 (r, k) — 可训练
    B:     LoRA 上投影矩阵 (d, r) — 可训练
    alpha: 缩放因子(超参数)
    r:     秩
    """
    # 原始路径 (冻结)
    h = x @ W_0

    # LoRA 路径 (可训练)
    # x → A(降维到r) → B(升维回d)
    lora_out = x @ A.T @ B.T

    # 合并, alpha/r 是缩放系数
    return h + (alpha / r) * lora_out

3.2.2 QLoRA:在 4-bit 量化上做 LoRA

QLoRA 的三个关键创新:

  1. 4-bit NormalFloat (NF4) 量化:基于正态分布的最优量化格式
  2. Double Quantization:量化参数本身也被量化,进一步省内存
  3. Paged Optimizers:利用 NVIDIA 统一内存在 GPU↔CPU 间自动换页
"""
QLoRA 实战:在单张 RTX 4070 (12GB) 上微调 LLaMA-3-8B
"""
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
import torch

# ===== 1. 4-bit 量化配置 =====
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,                  # 加载为 4-bit
    bnb_4bit_quant_type="nf4",          # NormalFloat4 量化类型
    bnb_4bit_compute_dtype=torch.bfloat16,  # 计算时用 BF16
    bnb_4bit_use_double_quant=True,     # 双重量化
)

# ===== 2. 加载量化模型 =====
model_id = "meta-llama/Meta-Llama-3.1-8B"

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",                  # 自动分配到可用 GPU
    torch_dtype=torch.bfloat16,
    attn_implementation="flash_attention_2",  # 使用 FlashAttention
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token

# ===== 3. 准备 QLoRA =====
model = prepare_model_for_kbit_training(model)  # 关键:冻结量化层, 开启梯度检查点

lora_config = LoraConfig(
    r=16,                    # 秩:通常 8-64, r 越大表达力越强但越贵
    lora_alpha=32,           # 缩放因子: 通常设为 2*r
    target_modules=[         # 作用于哪些模块
        "q_proj", "k_proj", "v_proj", "o_proj",   # Attention
        "gate_proj", "up_proj", "down_proj",        # FFN (MLP)
    ],
    lora_dropout=0.05,       # Dropout
    bias="none",             # 不训练 bias
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出示例: trainable params: 41,943,040 || all params: 8,030,261,248 || trainable%: 0.5222

# ===== 4. 数据集 =====
dataset = load_dataset("tatsu-lab/alpaca", split="train")

def format_instruction(example):
    if example.get("input"):
        text = f"""### Instruction:\n{example['instruction']}\n\n### Input:\n{example['input']}\n\n### Response:\n{example['output']}"""
    else:
        text = f"""### Instruction:\n{example['instruction']}\n\n### Response:\n{example['output']}"""
    return {"text": text}

dataset = dataset.map(format_instruction)

# ===== 5. 训练 =====
training_args = SFTConfig(
    output_dir="./qlora_output",
    num_train_epochs=3,
    per_device_train_batch_size=2,       # 12GB 显存, micro-batch=2
    gradient_accumulation_steps=8,       # 有效 batch_size = 2 * 8 = 16
    learning_rate=2e-4,                  # QLoRA 用较大学习率
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    optim="paged_adamw_8bit",           # 8-bit paged optimizer
    bf16=True,
    logging_steps=10,
    save_strategy="epoch",
    max_seq_length=1024,
    gradient_checkpointing=True,         # 梯度检查点: 省显存
    gradient_checkpointing_kwargs={"use_reentrant": False},
    dataset_text_field="text",
    report_to="wandb",                   # 可选: 实验追踪
)

trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
)

trainer.train()

# ===== 6. 保存 LoRA 权重 =====
trainer.save_model("./qlora_output/final")
# 注意: 保存的只有 LoRA 权重 (~80MB), 不是完整模型 (~16GB)

# ===== 7. 合并权重(可选,用于部署) =====
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    model_id, torch_dtype=torch.bfloat16, device_map="cpu"
)
merged_model = PeftModel.from_pretrained(base_model, "./qlora_output/final")
merged_model = merged_model.merge_and_unload()  # 合并 LoRA 到基础权重
merged_model.save_pretrained("./merged_model")
tokenizer.save_pretrained("./merged_model")

3.2.3 LoRA 超参数选择指南

参数推荐值说明
r (秩)8-32 (SFT), 64-128 (预训练续训)越大越接近全参数微调,但显存和计算开销线性增长
lora_alpha2 × r实际缩放比 = alpha/r,通常保持 ~2
target_modules全部线性层QKV + O + FFN 效果最好;只训 QV 也够用
lora_dropout0.05-0.1小数据集可以适当增大
learning_rate1e-4 ~ 5e-4比全参数微调高 5-10 倍
batch_size越大越好(受限于显存)用 gradient accumulation 模拟大 batch

3.3 VLM 架构详解

3.3.1 主流 VLM 架构

LLaVA 架构 (最经典的 VLM)
═══════════════════════

图像输入 文本输入
│ │
▼ ▼
┌─────────────┐ ┌──────────┐
│ Vision │ │ Tokenizer│
│ Encoder │ │ │
│ 冻结/微调 │ │
└─────┬───────┘ │
│ (257×1024) │ (seq_len × vocab_size)
▼ │
┌─────────────┐ │
│ Projection │ ← MLP(1024→4096) │
│ Layer │ 将视觉特征映射到 │
│ (连接器) │ LLM 的隐空间维度 │
└─────┬───────┘ │
│ (257×4096) │
▼ ▼
┌─────────────────────────────────────────────────┐
│ Language Model (LLM) │
│ │
│ │
│ ┌───┬───┬───┬─────┬───┬───┬───┬───┬───┐ │
│ │ v1│ v2│...│v257 │请 │描 │述 │图 │片 │ │
│ └───┴───┴───┴─────┴───┴───┴───┴───┴───┘ │
│ visual tokens text tokens │
│ │
│ → 自回归生成回答 │
└─────────────────────────────────────────────────┘

3.3.2 VLM 训练的两阶段

Stage 1: 预训练 (Feature Alignment)

  • 目标:让 Projection Layer 学会将视觉特征"翻译"到文本空间
  • 数据:大量图文对(如 LCS-558K,558K 图文对)
  • 设置:冻结 Vision Encoder + 冻结 LLM,只训练 Projection Layer
  • 时间:A100 × 8 约 5 小时

Stage 2: 指令微调 (Visual Instruction Tuning)

  • 目标:让模型学会遵从多模态指令(看图回答问题、描述图片等)
  • 数据:高质量视觉指令数据(如 LLaVA-665K)
  • 设置:冻结 Vision Encoder,训练 Projection Layer + 训练/LoRA LLM
  • 时间:A100 × 8 约 20 小时
"""
VLM (LLaVA 风格) 训练示例
使用 Hugging Face 生态
"""
from transformers import (
    LlavaForConditionalGeneration,
    AutoProcessor,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
import torch

# 1. 加载预训练的 LLaVA 模型
model_id = "llava-hf/llava-1.5-7b-hf"

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = LlavaForConditionalGeneration.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
)
processor = AutoProcessor.from_pretrained(model_id)

# 2. 只对 LLM 部分应用 LoRA
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)

# 3. 训练(数据处理需要特殊的 collator 处理图像)
# 完整实现参考: https://huggingface.co/docs/trl/main/en/sft_trainer#multi-modal

3.4 VLA 的特殊性:视觉-语言-动作的统一

3.4.1 VLA 架构核心思想

VLA 的本质是将动作预测视为一种"语言生成"任务:

RT-2 / OpenVLA 的核心架构:

  视觉观测 (Camera Image)        语言指令 ("pick up the red cup")
         │                                    │
         ▼                                    ▼
  ┌──────────────┐                    ┌──────────────┐
  │ Vision Encoder│                    │  Tokenizer   │
  │ (SigLIP/DINOv2)│                   │              │
  └──────┬───────┘                    └──────┬───────┘
         │                                    │
         ▼                                    ▼
  ┌──────────────┐                           │
  │ Projector     │                           │
  │ (MLP)         │                           │
  └──────┬───────┘                           │
         │                                    │
         ▼                                    ▼
  ┌─────────────────────────────────────────────────┐
  │                 LLM Backbone                     │
  │              (LLaMA / PaLM-E)                    │
  │                                                  │
  │  [visual_tokens] + [instruction_tokens]          │
  │         ↓                                        │
  │  自回归生成:                                      │
  │  [action_token_1] [action_token_2] ... [action_7]│
  │                                                  │
  │  每个 action_token 对应一个离散化的动作维度         │
  │  (从扩展词表中选择, 例如 token_id = 32000+bin_id)  │
  └──────────────────────────────────┬───────────────┘
                                     │
                                     ▼
                          反离散化 (Un-discretize)
                                     │
                                     ▼
                           连续动作: [dx, dy, dz, drx, dry, drz, grip]
                                     │
                                     ▼
                            发送到机械臂控制器

3.4.2 视觉特征图与动作空间的映射

关键问题:如何将 (H, W) 的 2D 视觉特征映射到 7-DOF 的动作空间?

"""
VLA 中视觉-动作对齐的核心思路
"""
import torch
import torch.nn as nn

class VLAModel(nn.Module):
    """
    简化的 VLA 模型架构
    展示视觉特征如何与动作空间映射到同一隐空间
    """
    def __init__(self, vision_encoder, llm_backbone, action_dim=7, num_bins=256):
        super().__init__()
        self.vision_encoder = vision_encoder  # e.g., SigLIP
        self.llm = llm_backbone               # e.g., LLaMA-7B

        # 视觉 → LLM 隐空间的投影
        self.visual_projector = nn.Sequential(
            nn.Linear(vision_encoder.hidden_dim, llm_backbone.hidden_dim),
            nn.GELU(),
            nn.Linear(llm_backbone.hidden_dim, llm_backbone.hidden_dim),
        )

        # 扩展词表: 为动作 token 添加 num_bins 个新 token
        # 原始词表: [0, vocab_size)
        # 动作词表: [vocab_size, vocab_size + num_bins)
        self.action_token_offset = llm_backbone.config.vocab_size
        llm_backbone.resize_token_embeddings(
            llm_backbone.config.vocab_size + num_bins
        )

        self.num_bins = num_bins
        self.action_dim = action_dim

    def forward(self, images, text_input_ids, action_labels=None):
        """
        images: (B, C, H, W)
        text_input_ids: (B, text_seq_len)
        action_labels: (B, action_dim) — 离散化后的 bin indices
        """
        # 1. 视觉编码
        with torch.no_grad():  # 通常冻结视觉编码器
            visual_features = self.vision_encoder(images)  # (B, num_patches, vis_dim)

        # 2. 投影到 LLM 空间
        visual_embeds = self.visual_projector(visual_features)  # (B, num_patches, llm_dim)

        # 3. 获取文本 embedding
        text_embeds = self.llm.get_input_embeddings()(text_input_ids)  # (B, text_len, llm_dim)

        # 4. 拼接: [visual_embeds, text_embeds]
        inputs_embeds = torch.cat([visual_embeds, text_embeds], dim=1)

        # 5. 通过 LLM 生成动作 token
        if action_labels is not None:
            # 训练模式: 将动作 label 转为 token id
            action_token_ids = action_labels + self.action_token_offset  # (B, action_dim)
            # 将动作 token 也附加到输入序列末尾(teacher forcing)
            action_embeds = self.llm.get_input_embeddings()(action_token_ids)
            full_embeds = torch.cat([inputs_embeds, action_embeds], dim=1)

            outputs = self.llm(inputs_embeds=full_embeds)
            # 只对动作 token 位置计算 loss
            # ...
            return outputs
        else:
            # 推理模式: 自回归生成 action_dim 个 token
            generated_tokens = []
            current_embeds = inputs_embeds

            for _ in range(self.action_dim):
                outputs = self.llm(inputs_embeds=current_embeds)
                next_token_logits = outputs.logits[:, -1, :]

                # 只从动作词表中采样
                action_logits = next_token_logits[:, self.action_token_offset:
                                                   self.action_token_offset + self.num_bins]
                next_token = action_logits.argmax(dim=-1)
                generated_tokens.append(next_token)

                # 更新输入
                next_embed = self.llm.get_input_embeddings()(
                    next_token + self.action_token_offset
                ).unsqueeze(1)
                current_embeds = torch.cat([current_embeds, next_embed], dim=1)

            # 反离散化
            action_bins = torch.stack(generated_tokens, dim=1)  # (B, action_dim)
            continuous_actions = action_bins.float() / (self.num_bins - 1) * 2 - 1  # → [-1, 1]
            return continuous_actions

3.4.3 VLA vs VLM vs LLM 训练差异总结

维度LLMVLMVLA
输入模态文本文本 + 图像文本 + 图像 + 本体感受
输出模态文本 token文本 token动作 token (离散) / 连续向量
Vision EncoderCLIP / SigLIPSigLIP / DINOv2
额外模块Visual ProjectorVisual Projector + Action Head
词表标准文本词表标准 + <image>标准 + <image> + 动作 bins
训练数据海量文本图文对 + 视觉指令机器人轨迹数据
关键难点规模与数据质量模态对齐动作空间归一化 + 实时推理
推理延迟要求低(可流式)极高(实时控制 ≤ 50ms)

4. 针对性实战案例 (Practical Use Cases)

4.1 LLM 路径:行业垂直化训练

4.1.1 场景:构建法律领域 LLM

完整路线图:

Step 1: 选择基座模型 (Base Model)
    ├── 英文: Llama-3.1-8B / Mistral-7B-v0.3
    ├── 中文: Qwen2.5-7B / Yi-1.5-9B / DeepSeek-V2-Lite
    └── 考量: 预训练语料中中文/法律占比, 上下文长度

Step 2: 继续预训练 (Continual Pre-training)
    ├── 数据: 法律法规全文 + 裁判文书 + 法学教材 + 律师问答
    ├── 规模: 10-50B tokens
    ├── 方法: 全参数或 LoRA(r=64+) 继续预训练
    └── 目的: 注入法律领域知识

Step 3: 指令微调 (SFT)
    ├── 数据: 法律咨询QA + 合同审查指令 + 案例分析 + 法条检索
    ├── 规模: 50K-500K 条高质量指令
    └── 方法: LoRA(r=16) 或 QLoRA

Step 4: 对齐 (DPO)
    ├── 数据: 法律回答的偏好对 (准确vs不准确, 专业vs口语化)
    ├── 规模: 10K-50K 对
    └── 目的: 确保引用准确, 不胡编法条

Step 5: 评估
    ├── 法律考试题 (司法考试真题)
    ├── 法条引用准确率
    └── 人工评估 (律师打分)

继续预训练的关键代码

"""
在法律语料上继续预训练 Qwen2.5-7B
使用 LoRA(r=64) 以适应消费级显卡
"""
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
from datasets import load_dataset

model_id = "Qwen/Qwen2.5-7B"
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16)
tokenizer = AutoTokenizer.from_pretrained(model_id)

# 继续预训练用较大的 r
lora_config = LoraConfig(
    r=64,                          # 继续预训练需要更大的 r
    lora_alpha=128,
    target_modules="all-linear",   # 所有线性层
    lora_dropout=0.0,              # 预训练阶段不用 dropout
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)

# 数据: 法律语料, 每条是纯文本
legal_dataset = load_dataset("text", data_files="legal_corpus/*.txt", split="train")

def tokenize_fn(examples):
    return tokenizer(
        examples["text"],
        truncation=True,
        max_length=4096,
        return_overflowing_tokens=True,   # 长文档自动切分
        return_length=True,
    )

tokenized = legal_dataset.map(tokenize_fn, batched=True, remove_columns=["text"])

# 训练配置
from trl import SFTConfig, SFTTrainer

config = SFTConfig(
    output_dir="./legal_cpt",
    num_train_epochs=2,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=32,   # 有效 batch = 32
    learning_rate=1e-4,               # 继续预训练的 LR
    lr_scheduler_type="cosine",
    warmup_steps=100,
    bf16=True,
    logging_steps=10,
    save_steps=500,
    max_seq_length=4096,
    dataset_text_field="text",
    gradient_checkpointing=True,
)

trainer = SFTTrainer(model=model, args=config, train_dataset=legal_dataset, tokenizer=tokenizer)
trainer.train()

4.1.2 Mistral 作为底座的优势

Mistral 系列模型(Mistral-7B, Mixtral-8x7B)有几个独特优势:

  • Sliding Window Attention (SWA):4096 窗口 + 滚动缓存,长文本更高效
  • GQA (Grouped-Query Attention):8 个 KV heads,推理更快
  • MoE (Mixture of Experts):Mixtral 实际激活参数仅 12.9B(总参数 46.7B),推理效率高
# Mistral 微调的注意事项
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "mistralai/Mistral-7B-v0.3",
    torch_dtype=torch.bfloat16,
    attn_implementation="flash_attention_2",  # 必须: FlashAttention-2 支持 SWA
)

# Mistral 使用 SentencePiece tokenizer, 词表 32K
# 如果中文场景需要扩展词表:
# 1. 训练中文 SentencePiece 模型
# 2. 合并词表
# 3. resize_token_embeddings()
# 4. 在中文语料上继续预训练(让新 embedding 学到语义)

4.2 VLA 路径:集成自己的机械臂

4.2.1 基于 OpenVLA 的定制化流程

OpenVLA 是目前最易用的开源 VLA 框架。

OpenVLA 定制化步骤:

1. 硬件抽象层适配
   ├── 定义你的机械臂的 action space (自由度, 关节范围)
   ├── 定义观测空间 (相机分辨率, 是否有深度)
   └── 计算归一化统计量

2. 数据采集
   ├── 使用遥操作 (teleoperation) 采集演示轨迹
   ├── 目标: 至少 50-100 条轨迹/任务
   └── 确保场景多样性 (光照, 物体位置, 背景)

3. 数据转换
   ├── 转为 RLDS (Reinforcement Learning Datasets) 格式
   └── 或转为 OpenVLA 的 JSON 格式

4. 微调
   ├── 在你的数据上 fine-tune OpenVLA
   ├── 冻结视觉编码器, LoRA 微调 LLM 部分
   └── 训练 action head

5. 部署
   ├── 推理延迟优化 (目标 < 100ms/step)
   └── 安全策略 (力矩限制, 碰撞检测)

4.2.2 适配自定义机械臂的代码框架

"""
为你的机械臂适配 OpenVLA
以 6-DOF UR5 + Robotiq 夹爪为例
"""
import numpy as np
from dataclasses import dataclass

@dataclass
class RobotConfig:
    """你的机械臂硬件配置"""
    name: str = "ur5_robotiq"
    action_dim: int = 7                     # 6-DOF + 1 gripper
    proprio_dim: int = 7                    # 本体感受维度
    camera_resolution: tuple = (224, 224)   # 图像输入分辨率
    control_frequency: int = 10             # 控制频率 (Hz)

    # 关节限位 (用于归一化)
    joint_limits_low: np.ndarray = np.array([-2*np.pi, -2*np.pi, -np.pi, -2*np.pi, -2*np.pi, -2*np.pi, 0.0])
    joint_limits_high: np.ndarray = np.array([2*np.pi, 2*np.pi, np.pi, 2*np.pi, 2*np.pi, 2*np.pi, 1.0])

    # 末端执行器工作空间范围 (用于笛卡尔控制)
    ee_pos_low: np.ndarray = np.array([-0.5, -0.5, 0.0])
    ee_pos_high: np.ndarray = np.array([0.5, 0.5, 0.5])


class RobotEnvironment:
    """机器人环境封装 (连接真实硬件或仿真器)"""

    def __init__(self, config: RobotConfig):
        self.config = config
        self.normalizer = ActionNormalizer(method="minmax")
        # 初始化机械臂连接 (URx driver / ROS / MuJoCo)
        # self.robot = URRobot("192.168.1.100")

    def get_observation(self) -> dict:
        """获取当前观测"""
        return {
            "image": self._capture_image(),          # (224, 224, 3) uint8
            "proprio": self._get_proprioception(),   # (7,) float32
        }

    def step(self, normalized_action: np.ndarray):
        """执行归一化的动作"""
        # 反归一化到真实动作空间
        real_action = self.normalizer.denormalize(normalized_action)

        # 安全检查
        assert self._is_safe(real_action), "Action exceeds safety limits!"

        # 发送到机械臂
        ee_delta = real_action[:6]    # 末端位姿增量
        gripper = real_action[6]       # 夹爪开合

        # self.robot.move_ee(ee_delta, acc=0.5, vel=0.3)
        # self.robot.set_gripper(gripper)

    def _is_safe(self, action: np.ndarray) -> bool:
        """安全策略: 检查动作是否在允许范围内"""
        ee_pos = action[:3]
        return np.all(ee_pos >= self.config.ee_pos_low) and \
               np.all(ee_pos <= self.config.ee_pos_high)


class VLAInferenceWrapper:
    """
    将 OpenVLA 模型封装为可直接控制机器人的接口
    """

    def __init__(self, model_path: str, robot_config: RobotConfig):
        from transformers import AutoModelForVision2Seq, AutoProcessor

        self.processor = AutoProcessor.from_pretrained(model_path)
        self.model = AutoModelForVision2Seq.from_pretrained(
            model_path,
            torch_dtype=torch.bfloat16,
            device_map="cuda",
        )
        self.robot_config = robot_config
        self.normalizer = ActionNormalizer(method="minmax")
        # 加载归一化统计量
        stats = np.load(f"{model_path}/action_stats.npz")
        self.normalizer.stats = {"min": stats["min"], "max": stats["max"]}

    def predict_action(self, image: np.ndarray, instruction: str) -> np.ndarray:
        """
        输入: RGB 图像 + 语言指令
        输出: 归一化动作 (7-DOF)
        """
        inputs = self.processor(
            text=instruction,
            images=image,
            return_tensors="pt",
        ).to("cuda")

        with torch.no_grad():
            # 生成 7 个动作 token
            action_tokens = self.model.generate(
                **inputs,
                max_new_tokens=self.robot_config.action_dim,
                do_sample=False,
            )

        # 解码动作 token → 归一化动作
        action = self._decode_action_tokens(action_tokens)
        return action

    def run_episode(self, instruction: str, max_steps: int = 300):
        """执行一个完整 episode"""
        env = RobotEnvironment(self.robot_config)

        for step in range(max_steps):
            obs = env.get_observation()
            action = self.predict_action(obs["image"], instruction)
            env.step(action)

            # 检查是否完成 (可用视觉检测或力传感器)
            if self._check_success(obs):
                print(f"Task completed in {step} steps!")
                break

4.2.3 RT-2 架构理解

RT-2(Robotics Transformer 2)由 Google DeepMind 提出,核心思想更简洁:

RT-2 = PaLI-X (VLM) + Action Tokenization

关键设计:
1. 使用已有的大规模 VLM (PaLI-X 55B) 作为骨干
2. 将机器人动作表示为文本 token:
   - 动作 [0.5, -0.3, 0.1, 0.0, 0.0, 0.1, 1.0]
   - → 离散化为 bin: [191, 89, 128, 128, 128, 140, 255]
   - → 编码为特殊 token: "191 89 128 128 128 140 255"

3. 训练时: 
   - 输入: 图像 + "pick up the red cup"
   - 目标输出: "191 89 128 128 128 140 255"
   - Loss: 标准的 next-token prediction

4. Co-fine-tuning:
   - 同时在 VQA 数据 + 机器人数据上训练
   - 保留 VLM 的视觉理解能力
   - 比例: ~50% VQA + ~50% robot data

关键 insight:RT-2 证明了 VLM 的视觉理解能力可以直接迁移到机器人控制。即使模型没见过特定物体,也能通过语义理解来操作它(如 "move the Taylor Swift figure to the right")。


5. 性能优化与本地部署 (Inference)

5.1 模型量化

训练完成后,模型需要量化以实现高效推理。

5.1.1 量化方法对比

                        量化精度 vs 模型质量
                        
   模型质量 ▲
   (Benchmark)│
              │ ★ FP16/BF16 (基准)
              │
              │   ★ GPTQ-4bit (几乎无损)
              │   ★ AWQ-4bit  (几乎无损)
              │
              │     ★ GGUF Q5_K_M (轻微下降)
              │
              │       ★ GGUF Q4_K_M (可接受)
              │
              │           ★ GGUF Q3_K_M (明显下降)
              │
              │               ★ GGUF Q2_K (不推荐)
              │
              └─────────────────────────────────▶ 压缩率
                   2x    3x    4x    5x    8x
方法原理优点缺点适用场景
GPTQ逐层量化, OBQ 二阶优化精度高, GPU 推理快需要校准数据集, 量化慢GPU 服务器部署
AWQ保护重要权重通道精度略优于 GPTQ, 更快同上GPU 服务器部署
GGUFllama.cpp 格式, 多种量化级别CPU 也能跑! 灵活GPU 利用率不如 GPTQ本地 / 边缘部署
bitsandbytes动态量化 (NF4/INT8)训练时可用 (QLoRA)仅推理时不是最优训练时的量化

5.1.2 GPTQ 量化实操

"""
使用 AutoGPTQ 量化模型
"""
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
import torch

model_id = "./merged_model"  # 你训练好的模型路径
quant_output = "./quantized_model_gptq"

# 1. 量化配置
quantize_config = BaseQuantizeConfig(
    bits=4,                  # 4-bit 量化
    group_size=128,          # 分组大小 (128 是平衡点)
    damp_percent=0.1,
    desc_act=True,           # 激活值排序 (更精确但更慢)
    model_file_base_name="model",
)

# 2. 加载原始模型
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoGPTQForCausalLM.from_pretrained(
    model_id,
    quantize_config=quantize_config,
    torch_dtype=torch.float16,
)

# 3. 准备校准数据集 (通常 128-512 条)
calibration_data = [
    tokenizer(text, return_tensors="pt", max_length=2048, truncation=True)
    for text in calibration_texts[:256]
]

# 4. 执行量化(7B 模型约 10-30 分钟)
model.quantize(calibration_data)

# 5. 保存
model.save_quantized(quant_output)
tokenizer.save_pretrained(quant_output)
# 量化后大小: 7B 模型 FP16=14GB → GPTQ-4bit ≈ 4GB

5.1.3 AWQ 量化(推荐)

"""
AWQ 量化 — 通常比 GPTQ 更快且精度略优
"""
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_id = "./merged_model"
quant_output = "./quantized_model_awq"

model = AutoAWQForCausalLM.from_pretrained(model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)

quant_config = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM",     # GEMM kernel, 推理更快
}

model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized(quant_output)
tokenizer.save_pretrained(quant_output)

5.1.4 GGUF 转换(用于 llama.cpp / Ollama)

# 克隆 llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# 安装依赖
pip install -r requirements.txt

# 1. HF 模型 → GGUF (FP16)
python convert_hf_to_gguf.py ../merged_model --outfile model-f16.gguf --outtype f16

# 2. 量化到不同精度
./llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M    # 推荐: 质量/大小平衡
./llama-quantize model-f16.gguf model-Q5_K_M.gguf Q5_K_M    # 更高质量
./llama-quantize model-f16.gguf model-Q8_0.gguf Q8_0        # 近乎无损

# GGUF 量化级别参考 (7B 模型):
# Q2_K:    2.7 GB  — 质量差, 不推荐
# Q3_K_M:  3.3 GB  — 可用
# Q4_K_M:  4.1 GB  — ★推荐★ 性价比最高
# Q5_K_M:  4.8 GB  — 高质量
# Q6_K:    5.5 GB  — 接近 FP16
# Q8_0:    7.2 GB  — 几乎无损
# F16:     14 GB   — 原始精度

5.2 推理引擎

5.2.1 vLLM:高性能 GPU 推理

vLLM 是目前最快的开源 LLM 推理引擎。

核心技术:PagedAttention

传统推理的 KV Cache 问题:
┌─────────────────────────────────────┐
│  Request 1: [██████░░░░░░░░░░░░░]  │  ← 大量内存碎片和浪费
│  Request 2: [████████████░░░░░░░]  │
│  Request 3: [██░░░░░░░░░░░░░░░░░]  │
│             已用    浪费(预分配)     │
└─────────────────────────────────────┘

vLLM PagedAttention:
┌─────────────────────────────────────┐
│  Page Table:                         │
│  Req1 → [Page3, Page7, Page1]       │  ← 按需分配, 像操作系统的虚拟内存
│  Req2 → [Page5, Page2, Page8, P4]   │
│  Req3 → [Page6]                     │
│  → 内存利用率 > 95%                  │
└─────────────────────────────────────┘
# vLLM 部署示例

# 1. 安装
# pip install vllm

# 2. 离线批量推理
from vllm import LLM, SamplingParams

llm = LLM(
    model="./merged_model",              # 你的模型路径
    # model="./quantized_model_awq",     # 或 AWQ 量化模型
    dtype="bfloat16",
    gpu_memory_utilization=0.90,         # GPU 显存使用率
    max_model_len=4096,
    tensor_parallel_size=2,              # 多卡张量并行
    # quantization="awq",               # 如果是 AWQ 模型
)

sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512,
    repetition_penalty=1.1,
)

prompts = [
    "请解释什么是 Transformer 架构",
    "写一首关于深度学习的诗",
]

outputs = llm.generate(prompts, sampling_params)
for output in outputs:
    print(output.outputs[0].text)
# 3. 启动 OpenAI 兼容 API 服务器
python -m vllm.entrypoints.openai.api_server \
    --model ./merged_model \
    --dtype bfloat16 \
    --api-key your-secret-key \
    --host 0.0.0.0 \
    --port 8000 \
    --max-model-len 4096 \
    --gpu-memory-utilization 0.9

# 然后用 OpenAI SDK 调用:
# curl http://localhost:8000/v1/chat/completions \
#   -H "Authorization: Bearer your-secret-key" \
#   -d '{"model": "./merged_model", "messages": [{"role":"user","content":"你好"}]}'

5.2.2 Ollama:最简单的本地部署

Ollama 是面向个人用户的本地 LLM 运行时,基于 llama.cpp。

# 1. 安装 Ollama
curl -fsSL https://ollama.com/install.sh | sh

# 2. 运行开源模型(自动下载)
ollama run llama3.1:8b
ollama run qwen2.5:7b
ollama run mistral:7b

# 3. 部署你自己的量化模型
# 创建 Modelfile
cat > Modelfile << 'EOF'
FROM ./model-Q4_K_M.gguf

# 设置模型参数
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 4096
PARAMETER repeat_penalty 1.1

# 系统提示
SYSTEM """你是一个专业的法律AI助手,基于中国法律法规提供咨询。请准确引用法条,不确定时明确告知。"""

# 对话模板 (根据你训练时使用的模板调整)
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>{{ end }}
<|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|>
<|start_header_id|>assistant<|end_header_id|>
{{ .Response }}<|eot_id|>"""
EOF

# 构建自定义模型
ollama create my-legal-llm -f Modelfile

# 运行
ollama run my-legal-llm

# 4. API 调用 (Ollama 默认在 11434 端口提供 API)
curl http://localhost:11434/api/chat -d '{
  "model": "my-legal-llm",
  "messages": [{"role": "user", "content": "劳动合同到期不续签,公司需要赔偿吗?"}],
  "stream": false
}'

5.2.3 推理优化技术总结

技术原理加速倍数实现方式
KV Cache缓存已计算的 Key/Value,避免重复计算2-5×所有框架默认开启
FlashAttention-2融合的 attention kernel,减少 HBM 访问1.5-3×attn_implementation="flash_attention_2"
Continuous Batching动态批处理,新请求不等待2-5× (吞吐)vLLM / TGI 默认
Speculative Decoding小模型草稿 + 大模型验证2-3×vLLM --speculative-model
Tensor Parallelism单层权重切分到多卡近线性tensor_parallel_size=N
量化 (INT4/INT8)低精度权重2-4×GPTQ/AWQ/GGUF
前缀缓存 (Prefix Caching)共享相同前缀的 KV Cache系统提示场景大幅加速vLLM --enable-prefix-caching

5.3 VLA 推理的特殊要求

VLA 模型用于实时控制机器人,推理延迟要求极其严格:

VLA 推理延迟预算:
┌─────────────────────────────────────┐
│ 控制频率: 10 Hz → 每步 100ms       │
│                                     │
│ ├── 图像采集:        ~5ms           │
│ ├── 图像预处理:      ~3ms           │
│ ├── Vision Encoder:  ~15ms          │
│ ├── LLM 推理:       ~50ms  ← 瓶颈  │
│ ├── 动作解码:        ~2ms           │
│ └── 通信 + 执行:    ~25ms           │
│                     ────            │
│ 总计:               ~100ms          │
└─────────────────────────────────────┘

优化策略:
1. 量化 LLM 到 INT4 → 推理加速 2-3x
2. 缓存语言指令的 KV Cache(指令不变时)→ 减少 30% 推理时间
3. 使用较小的 LLM backbone(3B 而非 7B)
4. TensorRT 编译 Vision Encoder → 加速 2-5x
5. Action Chunking: 一次预测多步动作 → 降低调用频率
"""
VLA 推理优化: Action Chunking
一次预测 k 步动作, 减少模型调用次数
"""
class ChunkedVLAInference:
    def __init__(self, model, chunk_size=4):
        self.model = model
        self.chunk_size = chunk_size
        self.action_buffer = []

    def get_action(self, image, instruction):
        if len(self.action_buffer) == 0:
            # 缓冲区为空, 调用模型预测 chunk_size 步动作
            actions = self.model.predict_actions(
                image, instruction, num_steps=self.chunk_size
            )  # 返回 (chunk_size, action_dim)
            self.action_buffer = list(actions)

        # 从缓冲区取出一步动作
        return self.action_buffer.pop(0)

附录

A. 推荐学习资源

论文(必读)

论文年份关键贡献
Attention Is All You Need2017Transformer 架构
LoRA2021低秩适配微调
QLoRA20234-bit 量化 + LoRA
LLaVA2023视觉指令微调
DPO2023直接偏好优化
RT-22023VLM → VLA
OpenVLA2024开源 VLA 框架
FineWeb2024大规模数据清洗方法论

Hugging Face 官方教程

实践仓库

  • LLaMA-Factory — 一站式 LLM 微调框架(支持 100+ 模型,GUI 界面)
  • Axolotl — 灵活的微调工具
  • Unsloth — 2x 加速的 LoRA 微调
  • LitGPT — Lightning AI 的预训练/微调工具

B. 常见问题排查

Q: OOM (Out of Memory) 怎么办?
A: 按优先级尝试:
   1. 减小 micro_batch_size (per_device_train_batch_size)
   2. 开启 gradient_checkpointing=True
   3. 增大 gradient_accumulation_steps (维持有效 batch size)
   4. 使用 QLoRA 而非 LoRA (4-bit 量化加载)
   5. 减小 max_seq_length
   6. 使用 DeepSpeed ZeRO-3 + CPU Offload
   7. 减小 LoRA rank (r=16 → r=8)

Q: Loss 不下降 / NaN?
A: 检查:
   1. 学习率是否太大 (SFT 通常 1e-5 ~ 5e-5, QLoRA 可以 1e-4 ~ 5e-4)
   2. 数据格式是否正确 (特别是 special tokens 和 chat template)
   3. 是否有数据污染 (训练数据中混入了评测数据)
   4. 使用 BF16 而非 FP16 (BF16 更不容易溢出)
   5. 检查 gradient clipping 是否生效

Q: 微调后模型变笨了 / 灾难性遗忘?
A: 
   1. 减小学习率
   2. 减少训练 epoch (1-3 epoch 通常足够)
   3. 在微调数据中混入通用数据 (10-20%)
   4. 使用较小的 LoRA rank
   5. 考虑使用 NEFTune (在 embedding 上加噪声)

Q: 多卡训练速度很慢?
A:
   1. 检查 NCCL 通信是否正常: NCCL_DEBUG=INFO
   2. 确认 NVLink 是否连接 (nvidia-smi topo -m)
   3. 减少通信频率: 增大 gradient_accumulation_steps
   4. 使用 BF16 而非 FP32
   5. 确认数据加载不是瓶颈: num_workers >= 4

C. 训练成本估算参考

任务硬件模型数据量预计时间预计成本 (云)
QLoRA SFT1×40907B50K 条2-4 小时~$5
QLoRA SFT1×40707B50K 条4-8 小时~$3
LoRA SFT1×A10013B100K 条6-12 小时~$30
Full SFT8×A1007B100K 条2-4 小时~$80
Continual PT8×A1007B10B tokens5-7 天~$5,000
Pre-train256×H1007B1T tokens2-3 周~$500,000
DPO1×A1007B10K 偏好对1-2 小时~$5

最后的建议

  1. 从 QLoRA 微调开始,不要一上来就预训练——除非你有明确的数据优势和算力预算。
  2. 数据质量 > 数据数量 > 模型大小。1000 条高质量指令微调出的模型,往往胜过 100K 条低质量数据。
  3. 先跑通 pipeline,再优化。用小数据集 + 小模型快速验证整个流程,确认可行后再 scale up。
  4. 善用开源社区:LLaMA-Factory 可以让你在 5 分钟内开始第一次微调,不需要写任何代码。
  5. 持续关注 Hugging Face:几乎所有最新的模型、数据集、训练工具都在这里首发。

最后更新: 2026-04-01 本笔记基于 PyTorch 2.3+, Transformers 4.43+, PEFT 0.12+, TRL 0.9+ 编写