1. Tools
- Teamwork: git & github + CI/CD
- Environment:
- Docker & Singularity
- Slurm
- Linux (Debian & Ubuntu & RockyLinux & Fedora)
- Windows & WSL2 (pin memory)
- CMake + vcpkg
- pip/conda/uv
- IDE: VSCode (Linux/Windows) & VS (Windows)
- Others: Bash & Powershell & Vim & Tmux
2. Languages & Frameworks
- Languages: C & C++ & CUDA & Python & pytorch & libtorch & Triton
- Inference: vLLM & sglang
- SFT: trl & unsloth
- RLHF: verl
- Compute Graph: MLIR
3. 基础 DL
3.1. MAE (L1) & MSE (L2) Loss
MAE, i.e., Mean Absolute Error:
$$ \text{MAE} = \frac{1}{n} \sum^{n}_{i=1}|\hat{y} - y| $$MSE, i.e. Mean Squared Error:
$$ \text{MAE} = \frac{1}{n} \sum^{n}_{i=1}(\hat{y} - y) $$MAE & MSE loss 通常被用于回归任务; 即单纯让模型的 output Tensor 中每个元素都数值上靠近 label Tensor, 不存在 “将 output 看成不同类别的概率” 这个操作.
比如有个模型的 output 是 [10, 4, 5] shape 的 tensor.
- 在分类任务中, 我们可能会把 10 作为 batch size (B), 4 作为 seq len (L), 5 作为 vocab size (V).
接着我们会对 V 维度做 softmax:outputs = torch.softmax(outputs, dim=-1), 这样每个 token 对应的 V 维度的 5 个值自然地被 map 到 (0, 1) 这个区间, 代表这个 token 是 0 号单词的概率, 是 1 号单词的 概率, … 是 5 号单词的概率.
这其实就是对 (10 * 4) 个 token 的分类问题, 一共有 5 类.- 但是在回归问题中, output 和 label 的 shape 一般相同 (每个维度都有意义, 每个元素都应该靠近 label).
例如, 场景是: batch size = 10, 预测一张 (4 * 5) 的图像内每个像素点的灰度值是多少.
这种情况下就应该直接将MAE(pred, label)或者MSE(pred, label)作为 loss 来优化.
- 惩罚强度不同。
MAE 对误差是线性惩罚,错 10 ⽐错 1 ⼤ 10 倍。
MSE 对误差是平⽅惩罚,错 10 ⽐错 1 ⼤ 100 倍。
所以 MSE 会更强烈地惩罚⼤误差。(即在优化时让模型更趋近 ground truth) - 对离群点的敏感性不同。
MAE 对 outlier 更稳健。 MSE 很容易被少数⼏个特别⼤的误差主导。
所以数据⾥如果有脏点、极端值,MAE 往往更稳。 - 优化性质不同。
MSE 更平滑,可导性更好,优化通常更稳定,尤其在深度学习⾥很常⻅。
MAE 在 $e=0$ 处不可导,⽽且梯度⼤⼩基本是常数,不像 MSE 会随误差变⼤⽽增⼤,所以优化时有时没那么“顺⼿”。 - 统计含义不同。
最⼩化 MSE 往往对应去拟合条件均值。
最⼩化 MAE 往往对应去拟合条件中位数。
所以如果你的⽬标更像“平均值预测”,MSE 常⻅;如果你更想抗异常值,MAE 常⻅.
⼀句话总结:
- L1 loss 适合你怀疑数据⾥有噪声点、离群点,或者你更想优化“中位数式”的稳健表现。
- L2 loss 适合误差近似⾼斯、你想让模型更重视⼤偏差,或者需要平滑、好优化的⽬标。
Code:
Click to Expand
| |
Which is the same as:
| |
3.2. Cross Entropy Loss and Binary Cross Entropy Loss
CE Loss 一般被用于分类任务; BCE Loss 一般被用于二分类任务.
3.2.1. Target Distribution Cross Entropy
假定我们要做一个分 NC 类的分类任务; 一般模型的 output logits 的 shape 为: [B, NC], 其中 B 为 batch size.
在 LLM 中, logits 一般理解为
[B, L, NC], 这只是 layout 的区别罢了, 因为本质上我们就是要为 (B * L) 的 token 分类.
在 Target Distribution Cross Entropy 中, label 的 shape 则为 [B, NC]: 即对于每个 sample, label 给出这个 sample 是第 $i (i \in [1, \text{NC}])$ 类的概率 (是一个 prob distribution).
这可能和你认为的经典分类任务不太一样: “每个 sample 不应该就是有一个确定的类别吗?” —— Wait, wait, wait! 谁说一个 sample 只可能有一个类别了? 比如我说 “Tom 的性别 label 是 [0.5, 0.2, 0.3], 即 50% 像男人, 20% 像女人, 30% 不知道” 呢? 此时我的模型是不是就该输出一个
[..., 3]的 Tensor, 并且尽可能向 [0.5, 0.2, 0.3] 靠近?
为计算 CE, 我们的第一步是对 logits 的 NC 维度 apply softmax, 进而转化为 probs: probs = torch.softmax(logits, dim=-1). 此时对于 probs: [B, NC] 中的每个 sample, 其 NC 个数字就代表了该 sample 是第 $i (i \in [1, \text{NC}])$ 类的概率.
注意: softmax 不仅能把
NC个数统一映射到 (0, 1), 还能保证他们的和为 1.
接着, 对于单个 sample, CE loss 的计算公式如下:
$$ L_\text{CE} = - \sum_{i=1}^{\text{NC}}(p_\text{label}^i \times \log{p_\text{pred}^i}) $$计算结果是一个标量; 而对于 B 个 sample, 最终的 CE Loss 就是将所有 sample 的 Loss 求和取平均.
看到这里, 我们会发现, CE Loss 与 L1/L2 Loss 都在 “将 output tensor 中的所有元素向 label 中的所有元素优化”.
但是他们有个很重要的不同: CE Loss 将最后一个维度 (NC) 对待为 “有NC种类别, 它们分别是什么概率”, 因此 apply 了 softmax; 而 L1/L2 loss 无论有多少维, 都会平等对待所有元素, 统一向 label 优化.
Code:
| |
3.2.1. 明确要分第几类任务下的 CE Loss (NLL Loss)
模型输出 logits 还是 [B, NC], 计算 probs: probs = torch.softmax(logits, dim=-1).
现在, label 不再是 [B, NC] 的 “概率矩阵” 了, 而是 [B] 的 “第几类” 矩阵 —— 每个元素的意义就是对应 sample 是第几类.
那么, 对于一个 sample, 我们是不是可以理解为, 是这个类型的 ground truth prob 就是 1, 不是这个类型的 ground truth prob 就是 0?
此时计算该 sample 的标准 CE Loss 就可以简化为:
$$ L_\text{CE} = - \sum_{i=1}^{\text{NC}}(\text{label}^i \times \log{p_\text{pred}^i}) $$这里的 $\text{label}$ 是一个 0/1 向量, 只有 “确实是这个类型” 的位置是 1, 其余都是 0.
3.2. L1 & L2 正则化
4. LLM Tech List
Click to Expand
- Inference:
- TP (Megatron)
- Quantization: PTQ & QAT & GPTQ
- Pruning: Unstructured & Structured
- Paged Attention & Flash Attention & MQA & GQA & DeepSeek Sparse Attention (MLA)
- Prefix Caching
- Continuous Batching
- Chunked Prefill
- Speculative Decoding
- Sampling: Top-k & Top-p & Temperature & Beam Search
- Training:
- Pretraining
- SFT
- RLHF: PPO & GRPO & DAPO
- PEFT: LoRA, Prefix Tuning, P-Tuning, Prompt Tuning
- Efficiency:
- Mixed Precision: FP16 & BF16 & TF32 & INT8
- Gradient Checkpointing
- ZeRO Optimizer: Stage 1, 2, 3, ZeRO Offloading
- DP (DDP vs FSDP), MP, PP
- Position Embedding:
- ROPE
- Cluster:
- Apptainer + Slurm + Docker + Module
- torchrun & deepspeed & accelerate & bitsandbytes
- NCCL & Gloo & MPI
- Ray
- InfiniBand
- Evaluation: HPCG
5. LLM 基础模型结构
上图展示了 Decoder-Only Model 的经典结构; 省略了每层 Decoder Layer 的 Residual 结构和 Attention 内部的 Causal Mask.
另外要注意, 最后输出的 logitis: (B, L, V) 在 sampling 阶段需要对 vocab 维度做 softmax 进而转化为 probs 或者 logprobs:
| |
5.1. Embedding
Embedding 是模型内部的一个层, 和模型一起训练, 能够将一个 token id (int scalar) map 到一个 tensor (of shape (H,)).
- 一个 word (str) 对应一个 token id (int scalar).
- 一个 token id 被 map 到一个 tensor of shape
(H,).- 一句话 -> list[int] -> a tensor of shape
(L, H), whereLis the number of token ids.- 一个 request -> 一句话
- 多个 request -> 组 batch, 把短的用 pad-token-id 补齐到最长的 -> a tensor of shape
(B, L, H).
5.2. LayerNorm vs RMSNorm vs BatchNorm
对于输入 Tensor [B,L,H], LayerNorm 和 RMSNorm 都在 H 维度做 token-wise normalization (也就是对每个 token 的 hidden state 独立 normalize).
LayerNorm:
\[ \mu = \frac{1}{d}\sum_i x_i,\qquad \sigma^2 = \frac{1}{d}\sum_i (x_i-\mu)^2 \]\[ \mathrm{LN}(x)_i = \gamma_i \frac{x_i-\mu}{\sqrt{\sigma^2+\epsilon}} + \beta_i \]RMSNorm:
\[ \mathrm{RMS}(x)=\sqrt{\frac{1}{d}\sum_i x_i^2+\epsilon} \]\[ \mathrm{RMSNorm}(x)_i = \gamma_i \frac{x_i}{\mathrm{RMS}(x)} \]
- LayerNorm:关心均值和方差
- RMSNorm:只关心整体幅值
从计算角度, LN 需要: 1. 求 mean; 2. 求 variance; 3. 做中心化. RMSNorm 只需要:1. 求平方均值; 2. 开根号; 3. 做缩放. 大语言模型有很多层,每一层都有 norm。即使 RMSNorm 每次只省一点点,叠加到几十上百层、长序列和大 batch 上,就会变成很明显的收益。所以从工程角度看,RMSNorm 是一种更划算的选择。
为什么不用 BN?
BN 需要在一个 batch 上统计均值和方差。但 NLP / LLM 中:
- 序列长度不同
- padding 很多
- token 分布差异很大
- batch 内样本语义高度异质
这会让 batch 统计变得噪声很大,也不稳定。
此外, 在推理时, batch 大小相比训练会小很多 -> 推理训练 batch normalize 数量级不同. 因此不适合 BN.
5.3. Rotary Position Embedding (RoPE)
为什么要 Position Embedding? 因为 Attention 机制对 token 的 position 不敏感; 需要注入 position 信息.
- Learned Absolute Position Embedding
最经典的一类。给第 1、2、3… 个位置各配一个可学习向量,然后与 token embedding 相加。BERT 一类早期模型常见。优点是简单;缺点是对训练长度之外的 extrapolation 较差,而且位置是“表驱动”的,泛化到更长上下文通常不理想 - Sinusoidal Positional Encoding
Transformer 原始论文里的做法。每个位置用不同频率的正弦/余弦函数编码。优点是无额外可学习参数,并且理论上可以推广到更长长度;但它是把位置加到 embedding 上,和 attention 机制本身耦合得没有那么直接. - Relative Position Encoding
不直接编码“第几个位置”,而更强调“两个 token 相距多远”。典型代表有 Shaw 等人的相对位置偏置、Transformer-XL 等。这类方法通常更贴近语言建模需求,因为很多语义关系更依赖相对距离而不是绝对索引。 - ALiBi (Attention with Linear Biases)
它不是显式给 embedding 加位置向量,而是在 attention score 上加一个和距离相关的线性 bias。优点是实现简单、长上下文外推通常不错。很多长上下文讨论里会把它拿来和 RoPE 对比。 - RoPE (Rotary Position Embedding)
它把位置信息编码进 Q/K 的旋转中。相比绝对位置相加,RoPE 更“贴着 attention 在工作”,因此在建模相对位置关系时非常自然。RoFormer 论文明确指出,RoPE 同时编码绝对位置,并在 self-attention 里显式体现相对位置依赖。
为什么现在很多 LLM 都用 RoPE?
- 它对相对位置建模很自然。语言任务里,很多关系并不取决于“这是第 137 个 token”,而取决于“它离前面的主语/谓语有多远”。RoPE 通过旋转后的内积,让 attention 更容易反映相对位置信息.
- 它对长上下文更友好。相比 learned absolute embedding,RoPE 在长度外推上通常更有优势,因此非常适合现代 LLM 追求更长 context window 的趋势。
5.4. Attention
$$ O=Attention(Q,K,V)=softmax\left(\text{Mask}(\frac{QK^T}{\sqrt{d_k}})\right)V $$- Causal Mask: 对于单句输入
I: (L, H)(忽略 Batch 维度), 经过 attention 最终得到的输出矩阵O: (L, H); 我们在训练中期待O[i, :]这个 token 是I[:i, :]对应的 next token; 因此无论在推理还是训练时, 都需要一个下三角矩阵 mask 掉QK: (L, H)的 “未来” 部分: 比如对于QK的第一行, 只有第一个值有效, 这样O[0, :]就表示了给模型输入的第一个 token, 它预测出的是什么 token. - Attention Mask: 标注哪些部分用来计算 attention, 比如 pad token 在 attention mask 中就应该标记为 0.
- 为什么除以 $\sqrt{d}$: 因为点积 \(q \cdot k\) 的方差会随着维度 \(d\) 增大而变大,不除以 \(\sqrt{d}\) 会让 softmax 输入过大、容易饱和,导致梯度变小、训练不稳定,所以要用 \(\sqrt{d}\) 把尺度归一化。
- MHA 是每个 query head 都有自己独立的 K/V head,GQA 是多个 query head 共享一组 K/V head,MQA 则是所有 query head 共用同一组 K/V head,所以它们本质上是在“KV 共享程度”和“效果与推理效率的权衡”上逐级加强。
5.5. Sampling
LMHead 的输出是 (B, L, V), V 是 vocab size, 后续要通过 sampling 过程选出具体是哪个 token:
- top-p sampling
- top-k sampling
- temperature sampling:
prob = prob / temp
一般 temp 设置为 0.6-0.8 模型的效果最好, 因为此时模型有更多的采样可能; 每次都只采纳最高 prob 的 token 会使效果变差.
- temp=1: 不改变模型 prob 分布.
- temp 越小: prob 分布越尖锐, 模型采样多样性越小.
- temp 越大: prob 分布越平滑, 多样性越大.
- temp=0: prob 没法除 0, 一般推理引擎把这种情况直接处理为 greedy sampling (只选 prob 最高的 token).
6. 推理引擎和典型加速策略
6.1. Model Parallel & Tensor Parallel & Pipeline Parallel
这三个并行策略主要争对 “大模型” (单卡放不下) 推理场景.
- MP: 一个 GPU 放不下一整个模型, 因此把模型按顺序切分放到不同 GPU 上串行执行; MP 是 transforers 库默认的并行策略; 缺点是串行执行不同 GPU 需要等待 (当前 GPU 需要等上一个 GPU 的数据传过来才能计算), 速度慢.
- TP: 把模型的一层 (可以认为是若干个矩阵计算操作, 例如一层是一个 Transformer Layer) 切分到不同 GPU 并行执行 (例如, $A \times B$ 可以切分成 $A_1 \times B$ 和 $A_2 \times B$ 并行算); 速度快并且能降低单卡的显存占用, 但需要手动修改矩阵计算的代码; 目前工业界已经对 TP 做到了很大的支持, 推理引擎 (例如 vllm, sglang) 都已经默认支持 TP; 但是由于 TP 需要模型使用 “支持 TP 计算” 的模型层定义, 因此可以看到推理引擎内都会额外自己定义一遍模型.
- PP: 可以理解为高级的 MP, 把模型的不同部分切分到不同 GPU, 而相比 MP 进一步做流水线优化 — 有些部分的计算不依赖于另一些部分的计算结果, 所以可以做到流水线并行; 目前对 PP 的支持都需要模型的开发者自己手动在推理引擎内部打补丁, 所以普及率远不如 TP.
6.2. 常用推理引擎
- vllm: 老牌开源引擎, 提出 paged attention (需要详细了解)
- sglang: 社区更加活跃, 提出 radix attention (了解即可)
- tensorrtLLM: NV 亲自支持, 纯 C++ 实现
6.3. 推理加速核心技术
- Prefill and Decode: Decode 能用之前的 KV Cache, 具体的自己搜一下吧.
- PagedAttention: 解决大量 request 不停申请 KV Cache 时, 显存上的 Memory Fragmentation 问题.
- 组 batch: 推理时所有 input 一定要组 batch, 通过对一个 batch 的 inputs 中较短 input_ids 填充 pad token id 进而组成
(B, L)的 input batch, 这里B就是表示有 B 个 request (一个 request 就是一个 input_ids). - Prefill-Decode Aggregation/Disaggregation: 是否 Prefill 和 Decode 一起算. 什么叫一起算: 当前 batch 同时包含 prefill request 和 decode request. 一起算的好处是能同时处理很多不同类型的 request, 在高并发的场景吞吐率高; 不一起算的好处是计算流程清晰, 计算效率更高.
- Continuous Batching: 批量 request 时的 decoding 阶段持续把新到请求动态插入同一批次 (假设设定死了
max_batch_size=10, 一个用户提交了 10 个 requests, 还没跑完时另一个用户又提交了 10 个 requests; 第一个用户提交的 10 个 requests 中有 8 个很快跑完了, 剩下 2 个一直跑不完; 这时候利用 Continuous Batching 技术能将第二个用户的 8 个 requets 和第一个用户的剩下 2 个 requets 组成一个新 batch 跑). - Chunked Prefill: 把长 prompt 的 prefill token ids 切开、让计算与其他请求交错执行以减少首 token 等待和显存峰值 (假设定死了 GPU 一次只能处理
100个 token, 如果有个 prefill input 占90个 token, 那当前 batch 其他 request 没法跑了, 所以把100个 token 切开一点点算, 不要一次全算). - Prefix Caching: 如果多个 request 共享 prefill token ids, 比如问十个一样的问题, 就没必要重复计算这十个问题的 KV Cache 了 — 我们为每个 prefill request 计算 hash value, 如果匹配上就利用之前计算过的 kv cache.
7. 训练
7.1. 训练的几个阶段
本节会大致讲一下几个阶段的特征, 这些特征都需要熟记.
Pre-training:
- 是资源消耗最大的阶段, 一般都要使用万卡集群. 所以学生/高校自己一般不会做预训练.
- 目的是让模型从 0 学会语义理解和说话.
- Tokenizer 中的 vocab mapping 是预训练前就决定好的, 但是模型内部的 embedding layer 是跟随预训练一起训的.
- 预训练完的模型叫 Base Model, 比如你在 Huggingface 上能找到的 Qwen/Qwen3-8B-Base 等. Base Model 具备说话和理解能力, 但是不知道何时听 (不停说), 而且输出的文本没有规范的格式, 不符合人类对话的偏好.
- Base Model 不支持 special tokens (比如
<|user|>,<|assistant|>, 一般在 tokenizer_config.json 文件里可以找到), 因此无法跑 chat completion task (chat completion task 就是用tokenizer.apply_chat_template方法把输入变成对话形式的 text). - [Interesting Fact] Qwen/Qwen2.5-7B 本身就是 Base Model; 而 Qwen/Qwen3-8B-Base 才是 Base Model, Qwen/Qwen3-8B 是 SFT+RL 过的 reasoning model! 自己跑实验的时候一定要查清楚这个模型是 Base 还是 Instruct Model.
Post-Training:
- 后训练的资源消耗一般是可接受的, 所以很多科研现在都在研究后训练.
- 后训练大体可以分为 SFT (Supervised Fine-Tuning) 和 RL (Reinforcement Learning) 两类, 主要区别是:
- SFT 会提供完整的 Question 和 Grount Truth Response, 模型每个 output token 都需要和 GT Response 的 token 算 Cross Entropy Loss. 形式化的说就是模型输出必须强制和答案对齐.
- 官方 SFT 训出来的模型一般叫 instruct model, 比如 Qwen/Qwen2.5-7b-Instruct; Qwen3 没有提供 Instruct 版本, 只提供了 Base 版本和 Reasoning 版本 (SFT+RL).
- SFT 完的模型, 根据你提供的 SFT 数据集, “能够” 具备对话能力 (chat completion), 知道何时合适地结束输出, 具有初步 reasoning 能力, 或者拥有专业领域的特殊知识. 你可以用不同的数据集多次 SFT 模型让他一点点有更多能力.
- SFT 分全量微调和 LoRA 微调, LoRA 下文会讲.
- RL 只会提供 Qusetion 和一个 “打分方式”, 训练的目标一般有两个, 1 是得到更高分, 2 是不要和上一轮的自己偏离太远.
- RL 完的模型, 由于能得到 “更高分”, 会更加符合人类偏好, 或者具有更强的 reasoning 能力. 训练什么能力取决于你对什么目标打分. Qwen/Qwen3-8B 就是 RL 过的模型.
7.2. Data Parallel & Distributed Data Parallel & Fully Sharded Data Parallel
这三个策略主要用于训练加速.
- DP: 每个 GPU 都有一份完整的模型副本, 每个 GPU 处理不同的数据 batch, 每个 GPU 计算完 loss 和梯度后, 需要把梯度同步到其他 GPU 上; DP 的优点是实现简单, 缺点是每个 GPU 都需要存储完整的模型副本, 显存占用大.
- DDP: 是 DP 的一种优化实现, 通过使用 NCCL 库进行高效的梯度同步 (RingAllReduce), 可以显著提升 DP 的效率; DDP 是 PyTorch 官方推荐的分布式训练方式. 使用 DDP 的方式是从 transformers 库中 import DDP 模块, 直接包裹模型; 另外 dataloader 需要一些额外配置 (例如 DistributedSampler) 来保证每个 GPU 处理不同的数据; 启动 DDP 我们一般会用
torchrun命令来 launch 训练脚本, 如果你有 4 个 GPU, 在 launch 时就指定启动 4 个进程 (进程对应一个 GPU); DDP 的缺点是每个 GPU 仍然需要存储完整的模型副本, 显存占用大. - FSDP: 是 DDP 的进一步优化, 通过把模型参数切分到不同 GPU 上 (Sharding), 可以显著降低每个 GPU 的显存占用; FSDP 的使用方式和 DDP 类似, 也是从 transformers 库中 import FSDP 模块, 直接包裹模型. FSDP 和 ZeRO 的三个 Stage 有千丝万缕的关系, 可以认为 FSDP 是 pytorch 官方实现的 ZeRO (详见下文 ZeRO).
7.3. ZeRO
Zero 优化技术看这篇: Zero Stage 1, 2, 3 (至少了解训练中有哪些显存占用, 每个 stage 优化的点是什么).
模型训练, 以 7B 模型为例:
(7B * fp16) Parameters + (7B * fp16) Gradients + (7B * fp32) * 3 Optimizer States + Activation (relevant to batch size and seq len)
- Zero 1: 切分 Optimizer State
- Zero 2: 切分 Gradients
- Zero 3: 切分 Parameters
7.4. Gradient Checkpoint
前向传播时不保存某些层的中间激活,只保留少量“检查点”;反向传播走到这段时,再用检查点重新做一遍前向,把缺失的激活现算出来,再继续求梯度。
更具体一点,通常是这样实现的:
把模型切成若干段: 例如一个很深的 Transformer,有 24 层,你可以每 4 层或每 6 层作为一个 segment。
前向时只保存 segment 边界的张量: 正常训练里,每层输出激活都会留着给 backward 用。checkpoint 后,只保存每段输入/输出,段内各层的中间激活不保留。
反向时按需重算该段前向: 当 backward 需要某一段内部某层的激活时,框架会拿这段的检查点输入,再临时跑一次这段 forward,恢复出中间激活,然后立刻计算梯度。
用“额外计算”换“更低显存”; 所以它本质上是:显存下降,训练时间上升。
常见情况是显存省很多,但计算开销增加大约 20% 到 40%,具体看切分方式。
7.5. SFT
SFT 我一般用 trl 或者 unsloth 进行训练. trl 是 huggingface 的官方库, unsloth 是 trl 的优化版, 自己写了一些 kernel, 并且自动做 memory offload.
LoRA 的具体做法是创建两个矩阵 (LoRA Adapter): B: (H, r) A: (r, H), (通常) 对 LLM 的每层 decoder 内的 q_proj 和 v_proj 做 LoRA:
原来: $Q = W_qX$, $V = W_vX$ —> 现在: $Q = W_qX + BAX$, $V = W_vX + BAX$
一般把 B 初始化为 0, A 随机初始化, 这样能保证开始训练前模型输出和原始一致.
7.6. DPO
7.7. PPO
7.8. GRPO
7.9. DAPO
- Clip Higher: GRPO -> $[1-\epsilon, 1+\epsilon]$, 上下界一样模型会收敛到一个策略就不再探索; 增加上界 (0.2 -> 0.28) 让模型往好的方向更自由探索.
- Token Level Loss: GRPO 的 adv 只在组内归一化, 长短序列 token 贡献被稀释; DAPO batch 归一化到 token.
- Overlong Filtering: 超长没结束的直接 mask 掉.
- Dynamic Sampling: 一整个 group 都错都对直接扔了.
8. HPC 相关
8.1. 一些基础名词
如果说 conda 是打包 python 环境, docker 和 singularity 就是打包系统环境, 包括你装的各种软件和库.
因此在 AI 领域, 为了让别人更容易复现自己的实验数据, 经常会把自己的实验环境打包成 docker 镜像给别人运行.
- Docker Image: 可以理解为系统环境的安装包, 不能实际运行, 需要用
docker run命令创建对应容器才能运行. - Docker Container: 运行了一个镜像的容器实例 — 可以理解为在你的操作系统里运行了一个 “新的子操作系统”, 你可以把它当成独立的服务器对待 (因此甚至能通过 ssh 登录你的 container!). Container 需要先用
docker start启动, 再用docker attach连接到其终端. - Singularity: 可以理解为一种更加安全的 docker 服务. Docker 不安全的原因是创建完容器, 在 container 内部, 用户是 root; 这会导致用户能够修改原本在外部无权修改的文件. Singularity 保证用户进入容器后的用户和外部各种权限保持一致.
- Slurm: 这是一种任务提交系统. 用户可以通过
slurm run申请若干 GPU, 若干 CPU, 指定运行时间, 指定 RAM 大小…… 当任务超时, 资源会被释放, 进程自动被 kill.
8.2. 如何在本地电脑跑 DL 代码
Windows 系统:
- 首先要安装 Nvidia 的驱动才能识别显卡. 驱动的作用是让 CPU 能够识别并且与外围设备通信.
- 安装了驱动后, 用
nvidia-smi命令 check 当前显卡状态.
- 安装了驱动后, 用
- 在 WSL2 上安装 Ubuntu. WSL 是 windows subsystem for linux, 由微软官方研发的 windows 下 linux 子系统.
- WSL1 的做法是由 Windows 系统作 Host, 上面跑一个不完全的 Linux 虚拟机; 这样做的坏处是 Linux 系统运行的非常慢.
- WSL2 将 Windows 和 Linux 直接抽象为两个平等的虚拟机, 并且重写了 Linux 内核, 让 Linux 系统的运行速度提升到近乎和原生系统类似; 虽然微软自己写了文件管理器的映射和显卡驱动的映射 (这对深度学习开发者来说极为友好, 否则 1: 你需要自己用
scp命令在 windows 和 linux 之间传文件, 2: 你需要在 linux 系统重新安装显卡驱动), 但是对外围设备的映射还是困难重重 (比如你自己的电脑上插了一个 u 盘, wsl2 里几乎没办法读到这个 u 盘的内容).
- 在 Unbuntu 内安装 docker, 启用 docker 服务, 然后
docker pull一些 image.- 如果你复现别人的代码, 别人提供了自己 build 好的包含代码运行环境的 image, 你就 pull 就完事了.
- 如果你想从零搭建一个环境, 一般我们会从 Docker Hub 上找 Nvidia 官方的 cuda repo, 在
Tags选项里找需要的 image. 比如我想用一个 ubuntu24 的 并且预装了 cuda13 的 image, 那我就会选择docker pull nvidia/cuda:13.1.1-cudnn-devel-ubuntu24.04. - 需要注意的是, 作为深度学习开发者, 我们一般不会 pull
runtime镜像, 只会 pulldevel镜像, 因为 runtime 镜像里面只包含了运行必要的 library, 无法利用 cuda 提供的接口进行二次开发 (比如你想用 cuda 的 lib 自己写新的算子, 就叫二次开发).
- 有了 image, 用
docker run创建一个容器. 这个容器就是实际运行的 “虚拟系统”. - 用
docker start启动创建的容器. - 本机打开 vscode -> attach to wsl -> attach to running container.
8.3. 如何在远程服务器跑 DL 代码
一定是用 ssh 连接到远程的 linux 系统:
- ssh 连接的方式是修改当前系统下
~/.ssh/config, 把远程系统的信息写进去. - 如果要免密连接:
- 在本机, 利用
ssh-keygen命令创建 private key + public key. (其实在哪里创建无所谓, 只要保证本机有 private key 就行). - 在远程系统, 将 public key 中的内容复制到
~/.ssh/authorized_keys这个文件中的新一行. (只有在这个文件里的 public key 对应 private key 可以免密登录). - 在本机, 在
~/.ssh/config的对应 Host 配置内将IdentityFile指定为本地的 private key 路径.
- 在本机, 利用
我这里举一个 ssh key 配置的例子:
| |
有了这个配置, 当我在本机运行 ssh REMOTE_MICK_NAME 时, 就能用 ~/.ssh/my-private-key, 以 jamesnulliu 这一用户身份, 登录 remote.ip.com.
能够登录远程系统后, 分两种情况:
- 如果远程系统本身就有 GPU, 即你用
nvidia-smi能看到 gpu:- 一般用 docker 创建 (
docker pull => docker run => docker start) 自己的容器来跑代码.
- 一般用 docker 创建 (
- 如果远程系统是计算集群, 本身没有 GPU, 要申请计算资源:
- 一般用 slurm 申请计算资源, 然后用 singularity 来创建容器 (当然你想用 docker 也是完全 ok 的, 只不过计算集群一般都只允许用户用 singularity), 运行代码.
8.4. GPU 相关
8.4.1. CUDA Core V.S. Tensor Core
CUDA Core 是 GPU 计算的基础单元, 每个时钟周期进行一次标量运算. 主要争对 FP32 和 FP64 的计算问题.
Tensor Core 是从 Volta 架构开始引入的新硬件单元, 每个时钟周期进行一次矩阵运算, 大大加快矩阵运算效率. 支持更低精度的运算 (FP16, BF16, INT8, …).
8.4.2. 历代架构图
| 架构名称 | 代表产品 | 算力版本 (CC) | 核心创新点 | 新支持的计算精度 |
|---|---|---|---|---|
| Volta | V100 | 7.0 | 第一代 Tensor Core,独立线程调度 | FP16, FP32 |
| Ampere | A100 | 8.0 | TF32, BF16, MIG (多实例 GPU), 结构化稀疏 | TF32, BF16, INT8 |
| Hopper | H100 | 9.0 | Transformer Engine (Gen 1), FP8, DPX 指令集, NVLink 4.0 | FP8, FP16, BF16 |
| Blackwell | B200 | 10.0 | Transformer Engine (Gen 2), FP4, 72-GPU NVLink 域 | FP4, FP6, FP8 |
| Rubin (还没出) | R100 | 11.0(?) | HBM4, 下一代 NVLink, 针对极致推理吞吐优化 | 高精度与低精度全覆盖 |
我们一般训练都会用算力 8.0 以上的 gpu, 因为 8.0 以上才支持 bf16 — bf16 比 fp16 训练稳定很多.