
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. 性能优化与…
FIELD_GUIDE
FIELD GUIDE
Use the guide rail to jump between sections.
目录
- 1. 硬件环境与底层框架 (The Bedrock)
- 2. 数据工程:模型的"燃料" (Data Pipeline)
- 3. 模型架构与训练范式 (Modeling)
- 4. 针对性实战案例 (Practical Use Cases)
- 5. 性能优化与本地部署 (Inference)
- 附录
1. 硬件环境与底层框架 (The Bedrock)
1.1 显存需求的第一性原理计算
在动手之前,你必须回答一个问题:我需要多少 GPU 显存?
1.1.1 参数存储
一个参数在不同精度下占用的字节数:
| 精度 | 每参数字节数 | 7B 模型参数占用 | 13B | 70B |
|---|---|---|---|---|
| FP32 | 4 bytes | 28 GB | 52 GB | 280 GB |
| FP16 / BF16 | 2 bytes | 14 GB | 26 GB | 140 GB |
| INT8 | 1 byte | 7 GB | 13 GB | 70 GB |
| INT4 | 0.5 byte | 3.5 GB | 6.5 GB | 35 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(梯度检查点) 将激活值显存降低到 ( 为层数),代价是约 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:如何选择?
| 维度 | FSDP | DeepSpeed 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 | 代表模型 | 词表大小 | 中文效率 |
|---|---|---|---|
| BPE | GPT-4, LLaMA | 32K-128K | 取决于训练语料比例 |
| SentencePiece (Unigram) | T5, mBART | 32K-250K | 较好 |
| WordPiece | BERT | 30K | 一般 |
关键决策:
- 如果做中文模型,务必确保 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-Embodiment | 100万+ 轨迹 | 22 种机器人 | 527 种技能 |
| DROID | 76K 轨迹 | Franka | 多样化操作 |
| BridgeData V2 | 60K 轨迹 | WidowX | 桌面操作 |
| RH20T | 110K 轨迹 | 多种 | 中国团队, 丰富场景 |
使用 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(自回归)
"""
预训练的核心循环(简化版)
注意: 真实预训练需要大量工程优化,这里展示核心逻辑
"""
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 年后主流方法)
其中 是偏好的(chosen)回答, 是不偏好的(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 的数学原理
核心思想:预训练模型的权重更新矩阵 是**低秩(Low-Rank)**的。
对于原始权重矩阵 ,LoRA 将更新分解为:
其中:
- ,
- (秩远小于原始维度)
- 被冻结,只训练 和
原始 W_q: 4096 × 4096 = 16,777,216 参数LoRA (r=16): 4096×16 + 16×4096 = 131,072 参数→ 仅原始的 0.78%!全模型:→ 仅总参数的 0.26%
初始化:
- 用高斯随机初始化
- 初始化为零矩阵
- 训练开始时 ,即模型从预训练权重出发
前向传播:
# 伪代码
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 的三个关键创新:
- 4-bit NormalFloat (NF4) 量化:基于正态分布的最优量化格式
- Double Quantization:量化参数本身也被量化,进一步省内存
- 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_alpha | 2 × r | 实际缩放比 = alpha/r,通常保持 ~2 |
target_modules | 全部线性层 | QKV + O + FFN 效果最好;只训 QV 也够用 |
lora_dropout | 0.05-0.1 | 小数据集可以适当增大 |
learning_rate | 1e-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 训练差异总结
| 维度 | LLM | VLM | VLA |
|---|---|---|---|
| 输入模态 | 文本 | 文本 + 图像 | 文本 + 图像 + 本体感受 |
| 输出模态 | 文本 token | 文本 token | 动作 token (离散) / 连续向量 |
| Vision Encoder | 无 | CLIP / SigLIP | SigLIP / DINOv2 |
| 额外模块 | 无 | Visual Projector | Visual 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 服务器部署 |
| GGUF | llama.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 Need | 2017 | Transformer 架构 |
| LoRA | 2021 | 低秩适配微调 |
| QLoRA | 2023 | 4-bit 量化 + LoRA |
| LLaVA | 2023 | 视觉指令微调 |
| DPO | 2023 | 直接偏好优化 |
| RT-2 | 2023 | VLM → VLA |
| OpenVLA | 2024 | 开源 VLA 框架 |
| FineWeb | 2024 | 大规模数据清洗方法论 |
Hugging Face 官方教程
- Hugging Face NLP Course — 从零学 Transformers
- 🤗 PEFT 文档 — LoRA/QLoRA/IA3 等所有参数高效方法
- 🤗 TRL 文档 — SFT/DPO/PPO/ORPO 训练
- 🤗 Accelerate 文档 — 分布式训练配置
- Alignment Handbook — 从 SFT 到 DPO 的完整对齐流程
- Open LLM Leaderboard — 模型评测排行榜
实践仓库
- 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 SFT | 1×4090 | 7B | 50K 条 | 2-4 小时 | ~$5 |
| QLoRA SFT | 1×4070 | 7B | 50K 条 | 4-8 小时 | ~$3 |
| LoRA SFT | 1×A100 | 13B | 100K 条 | 6-12 小时 | ~$30 |
| Full SFT | 8×A100 | 7B | 100K 条 | 2-4 小时 | ~$80 |
| Continual PT | 8×A100 | 7B | 10B tokens | 5-7 天 | ~$5,000 |
| Pre-train | 256×H100 | 7B | 1T tokens | 2-3 周 | ~$500,000 |
| DPO | 1×A100 | 7B | 10K 偏好对 | 1-2 小时 | ~$5 |
最后的建议:
- 从 QLoRA 微调开始,不要一上来就预训练——除非你有明确的数据优势和算力预算。
- 数据质量 > 数据数量 > 模型大小。1000 条高质量指令微调出的模型,往往胜过 100K 条低质量数据。
- 先跑通 pipeline,再优化。用小数据集 + 小模型快速验证整个流程,确认可行后再 scale up。
- 善用开源社区:LLaMA-Factory 可以让你在 5 分钟内开始第一次微调,不需要写任何代码。
- 持续关注 Hugging Face:几乎所有最新的模型、数据集、训练工具都在这里首发。
最后更新: 2026-04-01 本笔记基于 PyTorch 2.3+, Transformers 4.43+, PEFT 0.12+, TRL 0.9+ 编写