#标准答案题库(第二批:训练系统、推理系统与底层工程)
上面的“标准答案速查区”更偏基础原理与应用系统,这一节继续把训练系统、推理系统、显存优化、并行策略、FlashAttention、PagedAttention 等高频硬题补成显式问答版,方便直接背诵和查阅。
#21. DP / TP / PP 分别是什么?各自解决什么问题?
#标准答案
DP(Data Parallel,数据并行)是把同一个模型完整复制到多张卡上,每张卡处理不同数据分片,最后再同步梯度。它主要解决的是“如何扩大总 batch size 和训练吞吐”。
TP(Tensor Parallel,张量并行)是把同一层里的大矩阵切开,分到多张卡上共同计算。它主要解决的是“单卡放不下某一层”或者“单层计算太重”的问题。
PP(Pipeline Parallel,流水并行)是把不同层切到不同设备上,让 micro-batch 依次流过各 stage。它主要解决的是“模型太深,整网放不下一张卡”的问题。
一句话区分:
DP是按数据切;TP是按单层计算切;PP是按网络层切。
真实大模型训练里,通常不是三选一,而是混合并行。
#深度解析
1. 三种并行的切分维度对比
| 并行策略 | 切分维度 | 解决什么问题 | 通信开销 | 适用场景 |
|---|---|---|---|---|
| DP (Data Parallel) | 数据维度 (batch) | 扩大总 batch size | 每步 all-reduce 梯度 | 模型能放下单卡时 |
| TP (Tensor Parallel) | 模型维度 (单层内) | 单层太大放不下 | 每层 all-gather/reduce-scatter | 单层参数量 > 单卡显存 |
| PP (Pipeline Parallel) | 模型维度 (层间) | 模型太深放不下 | 每 stage 传输激活值 | 模型层数多、单卡放不下 |
2. 直观类比:三种并行像什么?
DP:多工厂复制
- 每个工厂有完整生产线
- 各自处理不同订单
- 每天下班汇总生产报告
TP:同一工厂内分工
- 一台机器太大,拆成多个工位
- 每个工位做一部分工序
- 工位间需要频繁传递半成品
PP:流水线
- 产品依次经过多个车间
- 每个车间负责一道工序
- 车间间用传送带运输
3. 为什么不能只用一种?
| 只用 DP | 模型 > 单卡显存 → OOM |
|---|---|
| 只用 TP | 通信太重,跨机效率低 |
| 只用 PP | Pipeline bubble,GPU 空等 |
4. 混合并行的典型配置
以 175B GPT-3 训练为例(假设 1024 张 A100):
总卡数 = DP × TP × PP = 8 × 8 × 16 = 1024
- TP = 8:同一层切到单机 8 卡(NVLink 高速互联)
- PP = 16:模型切成 16 段,每段 8 卡
- DP = 8:8 个副本并行处理不同数据
5. 面试官常见深挖追问
- "DP 的梯度同步具体怎么做?"
- 答:DDP(DistributedDataParallel)中,每张卡独立计算前向和反向。反向传播时,各卡计算本地梯度,然后通过 ring-all-reduce 同步:每张卡只发送/接收相邻卡的数据,N 步后所有卡都有完整平均梯度。通信量 = 2×(N-1)/N × 模型大小,和卡数几乎无关。
- "PP 的 bubble 怎么算?"
- 答:Pipeline bubble = (num_stages - 1) / num_micro_batches。例如 PP=4,micro-batch=8,bubble = 3/8 = 37.5%。意味着 37.5% 的时间里某些 GPU 在等待(空泡)。增加 micro-batch 数量可以减少 bubble。
- "如果模型 7B,8 卡 A100,最优并行策略是什么?"
- 答:7B FP16 约 14GB,8 卡 A100 (80GB) 完全放得下。首选纯 DP(DDP/FSDP),不需要 TP/PP。如果 batch size 很大导致激活值 OOM,加 FSDP 或 gradient checkpointing。
#22. 为什么大模型训练通常要做混合并行,而不是只用一种并行?
#标准答案
因为单一并行策略通常只能解决一个维度的问题,不能同时兼顾显存、算力利用率和通信成本。
- 只做
DP,模型太大时单卡根本放不下; - 只做
TP,跨卡通信会很重,而且对高速互联要求高; - 只做
PP,容易出现 pipeline bubble(流水线空泡),设备利用率不高。
所以大模型训练常见做法是:
- 用
TP/PP解决“模型放不下”的问题; - 再用
DP扩大总吞吐; - 如果是
MoE,还会再叠加 expert parallel(专家并行)。
混合并行的本质,就是在不同维度上拆解模型与数据,让总系统在“能放下 + 跑得动 + 通信可接受”之间取得平衡。
#深度解析
1. 单一并行策略的局限(定量分析)
假设训练 70B 模型,单卡显存 80GB:
| 只用 DP | 模型权重 140GB > 80GB → OOM |
|---|---|
| 只用 TP | 单机 8 卡可放,但跨机 TP 效率 <50% |
| 只用 PP | Bubble 37.5%(PP=4, micro-batch=8) |
2. 混合并行的通信量分析
| 策略组合 | 通信内容 | 通信频率 | 对带宽要求 |
|---|---|---|---|
| DP + TP | 梯度 + 激活分片 | 每步 + 每层 | 高(需 NVLink) |
| DP + PP | 梯度 + 跨 stage 激活 | 每步 + 每 micro-batch | 中 |
| TP + PP | 激活分片 + 跨 stage 激活 | 每层 + 每 micro-batch | 极高 |
| DP + TP + PP | 全部 | 全部 | 分层优化 |
3. 并行策略的分层原则
并行策略选择优先级:
第一层:能不能放下?
├─ 单卡放不下单层 → 必须上 TP
├─ 单层放得下但整模型放不下 → 上 PP
└─ 整模型放得下 → 只用 DP
第二层:通信怎么分层?
├─ TP 只在节点内(NVLink)
├─ PP 跨节点但同机柜(IB)
└─ DP 可跨机柜(以太网/IB)
第三层:收益最大化
├─ DP 扩大 batch size 提升吞吐
├─ TP 解决单层内存瓶颈
└─ PP 解决模型深度瓶颈
4. 实际案例:GPT-3 175B 的训练配置
模型:175B 参数
配置:96 层,d=12288,heads=96
硬件:1024 张 V100 (32GB)
并行策略:
- TP = 8(单机内,NVLink)
- PP = 16(跨机,IB)
- DP = 8(数据并行副本)
总卡数 = 8 × 16 × 8 = 1024
单卡负载:
- 每层参数:175B / 96 ≈ 1.8B
- TP 分片后:1.8B / 8 ≈ 225M(FP16 = 450MB)
- PP 分段后:96 / 16 = 6 层/卡
- 总权重:6 × 450MB = 2.7GB(远 < 32GB,显存充足)
5. 面试官常见深挖追问
- "如果 TP=8, PP=4, DP=2,总卡数是多少?每张卡存多少层?"
- 答:总卡数 = 8 × 4 × 2 = 64 卡。假设 32 层模型,PP=4 → 每 stage 8 层。TP=8 → 每层切到 8 卡。所以每张卡存 8 层的一部分(1/8 的每层参数)。DP=2 → 有两组这样的 32 卡 pipeline,处理不同数据。
- "混合并行中,通信怎么避免冲突?"
- 答:分层通信 + 通信重叠。1)TP 的 all-gather/reduce-scatter 在节点内走 NVLink;2)PP 的激活传输走 IB;3)DP 的 all-reduce 可以和其他计算重叠(如 backward compute 时同步前面层的梯度)。DeepSpeed 和 Megatron-LM 都有专门的通信调度器。
- "MoE 训练时, expert parallel 和 DP/TP/PP 怎么叠加?"
- 答:MoE 在 DP/TP/PP 基础上再加 EP(Expert Parallel)。EP 把不同 expert 放到不同卡上,路由器决定 token 去哪个 expert。常见配置:EP=DP(每个 DP 组内有自己的 expert 副本),TP=1(expert 内部不切),PP 按需要。DeepSpeed-MoE 和 Megatron-MoE 都支持这种 4D 并行。
#23. ZeRO-1 / ZeRO-2 / ZeRO-3 的区别是什么?
#标准答案
ZeRO(Zero Redundancy Optimizer)是一套通过分片 optimizer state(优化器状态)、gradient(梯度)、parameter(参数)来降低冗余显存占用的方法。
三者区别可以这样记:
ZeRO-1:只切 optimizer state;ZeRO-2:切 optimizer state + gradient;ZeRO-3:连 parameter 也一起切。
所以越往后,显存越省,但调度和通信也越复杂。
面试里的标准表达是:
ZeRO-1工程代价最低;ZeRO-2是较常见折中;ZeRO-3省显存最狠,但参数需要频繁 gather/scatter,通信与实现复杂度最高。
#深度解析
1. ZeRO 的分片对象与显存节省
假设 7B 模型,FP16,8 卡,Adam 优化器:
| ZeRO 级别 | 分片对象 | 每卡显存 | 总显存占用 | 节省 |
|---|---|---|---|---|
| 无 ZeRO | 无 | 权重+梯度+优化器 | ~120 GB/卡 | 基准 |
| ZeRO-1 | 优化器状态 | ~40 GB/卡 | ~320 GB | 3x |
| ZeRO-2 | 优化器状态 + 梯度 | ~30 GB/卡 | ~240 GB | 4x |
| ZeRO-3 | 优化器状态 + 梯度 + 参数 | ~20 GB/卡 | ~160 GB | 6x |
注意:总显存占用 = 单卡显存 × 卡数。ZeRO 不会减少"总"显存需求,只是把冗余分片到多卡。
2. ZeRO-1/2/3 的通信模式
| 级别 | 额外通信 | 通信量 | 对速度影响 |
|---|---|---|---|
| ZeRO-1 | 无(只在优化器 step 时分片更新) | 优化器状态分片 | <5% |
| ZeRO-2 | 梯度 reduce-scatter | 梯度大小 | 5-10% |
| ZeRO-3 | 前向 all-gather + 反向 reduce-scatter + 参数释放 | 参数大小 × 2 | 10-20% |
ZeRO-3 的通信最重:每次前向都要 all-gather 参数,反向完后释放(reshard)。
3. ZeRO-Offload:把显存压力转到 CPU/SSD
ZeRO-Offload 策略:
- 优化器状态 → 转到 CPU 内存
- 梯度 → 计算完立即转 CPU
- 参数 → 需要时从 CPU 加载到 GPU
代价:
- CPU-GPU 数据传输成为新瓶颈
- 训练速度下降 20-40%
- 但可以让单卡训练 10B+ 模型
4. 面试官常见深挖追问
- "ZeRO-3 和 FSDP 有什么区别?"
- 答:本质相同,都是把参数、梯度、优化器状态分片到多卡。区别在实现:ZeRO-3 是 DeepSpeed 的实现,FSDP 是 PyTorch 的原生实现。FSDP 的 API 更简洁(
FullyShardedDataParallel包装模型),和 PyTorch 生态集成更好。ZeRO-3 功能更丰富(如 Offload、Infinity 等)。
- 答:本质相同,都是把参数、梯度、优化器状态分片到多卡。区别在实现:ZeRO-3 是 DeepSpeed 的实现,FSDP 是 PyTorch 的原生实现。FSDP 的 API 更简洁(
- "为什么 ZeRO-1 的通信开销最小?"
- 答:因为 ZeRO-1 只分片优化器状态,而优化器状态只在参数更新时使用。前向和反向传播时,每张卡仍有完整参数和梯度,不需要额外通信。通信只发生在 optimizer.step() 时,同步优化器状态的分片更新。
- "如果 8 卡训练,ZeRO-3 每卡省多少显存?"
- 答:以 7B 模型为例,原始每卡约 120GB(FP16 权重 14GB + 梯度 14GB + 优化器状态 56GB + 激活)。ZeRO-3 后,每张卡只存 1/8 的参数、1/8 的梯度、1/8 的优化器状态 → 每卡约 15GB(不含激活)。加上激活值,实际每卡约 30-40GB,足够在 80GB A100 上运行。
#24. FSDP 和 DDP 有什么区别?
#标准答案
DDP(Distributed Data Parallel)本质上还是“每卡一份完整模型副本”,只是在反向传播时做高效梯度同步。它简单、稳定、成熟,但模型很大时显存压力大。
FSDP(Fully Sharded Data Parallel)则会把参数、梯度、优化器状态都做更彻底的分片,让每张卡只保留自己那一份,从而显著降低显存占用。
最稳的回答是:
DDP更简单,适合模型还能放下的场景;FSDP更省显存,适合超大模型训练;- 但
FSDP的参数 all-gather、reshard 调度更复杂,对通信更敏感。
#深度解析
1. DDP 与 FSDP 的显存对比
以 7B 模型,FP16,8 卡为例:
| 组件 | DDP (每卡) | FSDP (每卡) | 说明 |
|---|---|---|---|
| 模型权重 | 14 GB | 1.75 GB (1/8) | FSDP 分片 |
| 梯度 | 14 GB | 1.75 GB (1/8) | FSDP 分片 |
| 优化器状态 | 56 GB | 7 GB (1/8) | FSDP 分片 |
| 激活值 | 20 GB | 20 GB | 相同 |
| 总计 | ~104 GB | ~30 GB | 节省 3.5x |
2. FSDP 的生命周期
FSDP 包装每个层:
1. 初始化:把层的参数切成 N 份(N=卡数),每卡存一份
2. 前向:
- all-gather:把参数从各卡收集成完整参数
- 计算前向
- 释放完整参数(可选,取决于配置)
3. 反向:
- all-gather:再次收集完整参数(如果之前释放了)
- 计算反向,得到本地梯度
- reduce-scatter:把梯度分片到各卡
4. 优化器 step:
- 每卡只更新自己负责的那块参数
3. FSDP vs ZeRO-3
| 特性 | FSDP (PyTorch) | ZeRO-3 (DeepSpeed) |
|---|---|---|
| API | FullyShardedDataParallel(model) |
deepspeed.initialize() |
| 生态 | PyTorch 原生 | DeepSpeed 专属 |
| 功能 | 较简洁 | 更丰富(Offload、Infinity) |
| 性能 | 略优(原生优化) | 接近 |
| 易用性 | 高 | 中(需写 config) |
4. 面试官常见深挖追问
- "FSDP 为什么比 DDP 慢?"
- 答:FSDP 有额外通信开销:前向时的 all-gather 参数,反向时的 reduce-scatter 梯度。DDP 只在反向时 all-reduce 梯度一次。但 FSDP 的显存节省让 batch size 可以更大,实际吞吐可能反而更高。
- "FSDP 可以和 checkpointing 一起用吗?"
- 答:可以,两者正交。FSDP 分片参数/梯度/优化器;checkpointing 减少激活值显存。一起用可以让更大的模型在有限的卡上训练。
- "FSDP 的
auto_wrap_policy是什么?"- 答:FSDP 需要决定"哪些层一起包装"。
auto_wrap_policy自动识别 Transformer 的层边界,确保每个 FSDP 单元包含完整的 Attention + FFN 块。包装太细 → 通信太频繁;包装太粗 → 内存峰值高。
- 答:FSDP 需要决定"哪些层一起包装"。
#25. activation checkpointing(激活重计算)为什么能省显存?代价是什么?
#标准答案
训练时,反向传播需要用到前向阶段保存的中间激活。如果把所有激活都留着,显存会很大。
activation checkpointing 的思路是:
- 前向时不保存所有中间激活,只保存少量 checkpoint;
- 反向传播时,如果需要某段激活,就重新做一次该段前向计算,把它算回来。
所以它能省显存,本质上是“少存激活,多算一次前向”。
它的代价是:
- 训练时间会变长;
- 计算量会增加;
- 如果本来就 compute-bound(计算受限),代价会更明显。
一句话总结:activation checkpointing 是典型的“用算力换显存”。
#深度解析
1. 激活值显存的精确计算
无 checkpointing 时,每层需要保存的激活:
- Attention: Q, K, V, score, softmax_output, dropout_mask
- FFN: intermediate, activation_output, dropout_mask
- LayerNorm: 输入(用于反向计算)
总激活显存 ≈ batch_size × seq_len × layers × hidden_dim × factor
其中 factor ≈ 8-12(取决于具体实现)
示例:batch=4, seq=2048, L=32, d=4096
激活显存 ≈ 4 × 2048 × 32 × 4096 × 10 × 2 bytes ≈ 20 GB
2. Checkpointing 的策略
| 策略 | 保存内容 | 显存节省 | 计算代价 | 适用场景 |
|---|---|---|---|---|
| Full Checkpointing | 每层的输入 | ~70% | 重算整个前向 | 通用 |
| Selective Checkpointing | 只保存 Attention 输入 | ~40% | 只重算 Attention | FFN 计算量大时 |
| No Checkpointing | 全部激活 | 0% | 无 | 显存充足 |
3. 为什么计算代价是 "20-30%" 而不是 "100%"?
无 checkpointing:
前向: 1x 计算 → 保存激活
反向: 1x 计算
总计: 2x
checkpointing:
前向: 1x 计算 → 只保存输入
反向: 1x 计算 + 重算被 checkpoint 的部分 (0.3-0.5x)
总计: 2.3-2.5x
额外代价: (2.5 - 2) / 2 = 25%
4. 面试官常见深挖追问
- "checkpointing 和重计算有什么区别?"
- 答:是同一个东西的不同叫法。checkpointing = 保存检查点(每层输入),反向时重计算(recompute)从检查点到当前位置的激活。PyTorch 中
torch.utils.checkpoint就是实现这个。
- 答:是同一个东西的不同叫法。checkpointing = 保存检查点(每层输入),反向时重计算(recompute)从检查点到当前位置的激活。PyTorch 中
- "什么情况下 checkpointing 的代价会超过 30%?"
- 答:当模型已经很 compute-bound 时(如大 batch size、长序列)。因为 GPU 计算单元已经饱和,额外的重计算只能排队等待,无法被其他操作掩盖。此时时间代价可能达到 40-50%。
- "checkpointing 和 ZeRO 可以一起用吗?"
- 答:可以,而且常一起用。ZeRO 解决参数/梯度/优化器状态的显存;checkpointing 解决激活值的显存。两者正交,互不冲突。例如:ZeRO-2 + checkpointing 可以让 7B 模型在 8×V100 (32GB) 上训练。
#26. 为什么多卡训练通常不是线性加速?
#标准答案
理想情况下卡数翻倍,吞吐也翻倍;但现实中通常做不到,原因主要有四类:
- 通信开销:梯度同步、参数 gather、激活传输都会花时间;
- 负载不均:不同卡、不同 stage 计算量不完全一样;
- 输入流水不稳:数据加载、预处理、host 到 device 传输可能跟不上;
- 串行瓶颈:某些阶段无法完全并行,Amdahl 定律会限制整体加速比。
所以面试里要强调:多卡训练是”计算 + 通信 + 调度”联合系统问题,不是单纯堆卡就行。
#深度解析
1. Amdahl 定律:并行加速的理论上限
加速比 = 1 / (s + p/n)
其中:
- s:串行部分比例(无法并行)
- p:可并行部分比例(p = 1 - s)
- n:并行度(卡数)
即使 n → ∞,加速比上限 = 1/s
示例:如果 5% 的操作必须串行(如数据加载、checkpoint 保存),理论最大加速比只有 20 倍,无论用多少卡。
2. 四类瓶颈的定量感受
假设单机 8×A100,训练 7B 模型:
| 瓶颈类型 | 具体表现 | 对加速比的影响 |
|---|---|---|
| 通信开销 | all-reduce 梯度同步,每次通信 ~100ms | 每步多花 10-20% 时间 |
| 负载不均 | Pipeline Parallel 中某些 stage 计算量大 | 整批等待最慢的 stage |
| 数据流水不稳 | CPU 预处理跟不上 GPU 消耗 | GPU 空等,利用率 <90% |
| 串行瓶颈 | checkpoint 保存、日志记录 | 每 N 步一次,平均摊薄 |
3. 通信开销的详细拆解
以 DDP 的 all-reduce 为例:
每步训练:
1. 前向传播(计算)
2. 反向传播(计算)
3. all-reduce 梯度同步(通信)
通信占比 = 通信时间 / (计算时间 + 通信时间)
| 卡数 | 通信占比(典型值) | 实际加速比 |
|---|---|---|
| 2 | ~5% | 1.9x |
| 4 | ~15% | 3.4x |
| 8 | ~25% | 6.0x |
| 16 | ~35% | 10.4x |
| 32 | ~45% | 17.6x |
(注:实际值高度依赖模型大小、网络带宽、batch size)
4. 提升线性加速比的工程手段
| 手段 | 原理 | 效果 |
|---|---|---|
| 梯度累积 | 多步计算后一次通信 | 减少通信频率 |
| 通信重叠 | 反向传播时同步前面层的梯度 | 隐藏通信延迟 |
| 更大的 batch size | 每步计算量增大,通信占比相对减小 | 提升计算/通信比 |
| 更优的并行策略 | DP + TP + PP + FSDP 组合,减少跨机通信 | 降低通信总量 |
| 更高效的数据加载 | 多进程 dataloader、pin_memory、prefetch | 消除数据瓶颈 |
5. 面试官常见深挖追问
- ”如果 8 卡训练只有 5x 加速,怎么定位瓶颈?”
- 答:分三步定位:1)看 GPU 利用率(nvidia-smi dmon)→ 如果利用率低,可能是数据加载或通信瓶颈;2)看通信时间(torch profiler / nsys)→ all-reduce 占比高则优化通信;3)看 timeline → 是否有某些卡/某些 step 特别慢(负载不均)。常见根因:batch size 太小、数据加载慢、通信没重叠、Pipeline bubble。
- ”为什么模型越大,多卡扩展效率反而越高?”
- 答:因为大模型每步的计算量(FLOPs)大,而通信量(梯度大小)增长相对慢(∝ 参数量,不是 ∝ 计算量)。所以大模型的”计算/通信比”更高,通信占比更小,扩展效率更好。例如 1B 模型 8 卡可能只有 4x 加速,70B 模型 8 卡可能有 7x+ 加速。
- ”Pipeline Parallel 为什么不如 Data Parallel 扩展性好?”
- 答:Pipeline Parallel 把模型按层切到不同卡上,每张卡只算一部分层。问题:1)bubble:前向和反向传播时某些卡会空闲;2)负载不均:不同层的计算量不同;3)micro-batch 调度复杂。而 Data Parallel 每张卡都有完整模型,只是数据不同,没有 bubble 问题,扩展性更好(但受显存限制,大模型无法用纯 DP)。
#27. 为什么 TP 对高速互联特别敏感?
#标准答案
因为 TP 是把同一层的矩阵运算切到多张卡上做,这意味着一次前向/反向里就要频繁在卡间交换中间结果。
比如 attention 或 FFN 做张量切分后,经常需要:
- all-reduce
- all-gather
- reduce-scatter
如果互联带宽不够、延迟太高,通信就会吞掉本来省下来的计算时间,导致 TP 效率大幅下降。
所以标准回答是:
TP适合单机多卡、NVLink/NVSwitch 这类高速互联环境;- 如果跨机网络较弱,
TP往往不如DP/FSDP/PP那么稳。
#深度解析
1. TP 的通信模式详解
以 FFN 的 TP 切分为例(2 卡):
输入 X: (batch, seq, d)
卡 0: X @ W_0 → Y_0: (batch, seq, d/2)
卡 1: X @ W_1 → Y_1: (batch, seq, d/2)
需要 all-gather: 把 Y_0 和 Y_1 拼成完整 Y: (batch, seq, d)
每一层都要做一次 all-gather(前向)和一次 reduce-scatter(反向)。
2. 不同互联带宽的实际影响
| 互联类型 | 带宽 | TP 效率 | 适用场景 |
|---|---|---|---|
| NVLink | 900 GB/s | 95%+ | 单机 8 卡,TP 最优 |
| PCIe 4.0 x16 | 32 GB/s | 60-80% | 单机多卡,TP 可接受 |
| InfiniBand | 200-400 Gb/s (25-50 GB/s) | 40-60% | 跨机,TP 效率低 |
| 以太网 | 10-100 Gb/s (1-12 GB/s) | <30% | 跨机,不建议用 TP |
注意:NVLink 带宽是 InfiniBand 的 10-20 倍,这就是为什么 TP 必须绑在单机内。
3. 为什么 DP/FSDP/PP 对互联要求没那么高?
| 并行策略 | 通信频率 | 通信量 | 对带宽敏感度 |
|---|---|---|---|
| DP | 每步一次 all-reduce | 梯度大小(=参数量) | 中 |
| FSDP | 前向 all-gather + 反向 reduce-scatter | 参数分片大小 | 中-高 |
| PP | 每 micro-batch 一次激活传输 | 激活值大小 | 中 |
| TP | 每层多次 all-reduce/all-gather | 中间激活大小 | 极高 |
TP 的通信频率是"每层多次",而 DP 是"每步一次"。频率高 + 延迟敏感 → 必须高速互联。
4. 工程实践:混合并行中的 TP 定位
典型 70B 模型训练配置(8 节点 × 8 卡 = 64 卡 A100):
节点内(NVLink):
- TP = 8(同一层切到 8 卡)
节点间(InfiniBand):
- PP = 4(模型按层切 4 段,每段 16 卡)
- DP = 2(两段之间数据并行)
关键原则:
- TP 只在节点内(NVLink)
- PP/DP 跨节点(IB)
5. 面试官常见深挖追问
- "如果只有 PCIe,不用 NVLink,TP 还能用吗?"
- 答:可以用,但效率低。PCIe 带宽 (~32 GB/s) 远低于 NVLink (~900 GB/s)。对于小模型或低序列长度,PCIe 可能够用;但对于大矩阵乘法(如 8192×8192),TP 的 all-reduce 会成为瓶颈。实际中,纯 PCIe 环境更推荐用 FSDP + PP,避免 TP。
- "TP 的通信量具体怎么算?"
- 答:以 attention 的 TP 切分为例,输入 X: (b, s, d),切成 2 份。前向时每张卡算一半 head 的 attention,然后 all-gather 输出。通信量 = 输出大小 = b × s × d × 2 bytes (FP16)。对于 b=4, s=2048, d=4096,通信量 = 4 × 2048 × 4096 × 2 = 64 MB。每层都要传 64MB,32 层就是 2GB/步。
- "为什么 TP 通常只切 Attention 和 FFN,不切 Embedding?"
- 答:Embedding 层参数量大(V×d),但计算量小(只是查表)。切 Embedding 的收益低(计算不密集),但复杂度极高(词表需要在卡间同步)。所以 TP 通常只切计算密集的 Linear 层。
#28. all-reduce / all-gather / reduce-scatter 的区别是什么?
#标准答案
这三个都是分布式训练里很常见的通信原语。
all-reduce:先聚合再广播,结果每张卡都有一份完整归约结果。最典型用途是梯度同步。all-gather:每张卡把自己的分片发出来,最后每张卡都拿到拼完整后的全量结果。典型用途是参数或激活分片重组。reduce-scatter:先做归约,再把结果切分发回各卡。它相当于“all-reduce + scatter”的合体,在分片训练里很常见。
最简记法:
all-reduce是“大家算完一份公共答案,每人都拿完整答案”;all-gather是“大家把各自碎片拼起来,每人都拿完整拼图”;reduce-scatter是“大家一起汇总,但每人只拿自己负责那一块结果”。
#深度解析
1. 三种通信原语的数学定义
假设有 N 张卡,每张卡有一个向量 x_i:
| 原语 | 操作 | 结果(每张卡得到) | 典型用途 |
|---|---|---|---|
| all-reduce | y = sum(x_1, ..., x_N) |
完整的 y |
梯度同步(DDP) |
| all-gather | y = concat(x_1, ..., x_N) |
完整的 y |
参数重组(FSDP 前向) |
| reduce-scatter | y_i = sum(x_1, ..., x_N)[i] |
第 i 块 |
梯度分片(FSDP 反向) |
2. Ring 算法:高效实现 all-reduce
Ring All-Reduce(N 张卡,数据分 N 块):
Step 1 (Scatter-Reduce):
卡 0: 发送 block_1 给卡 1,接收 block_N 从卡 N-1
卡 1: 发送 block_2 给卡 2,接收 block_0 从卡 0
...
每轮每个卡对接收到的块做归约
Step 2 (All-Gather):
每个卡把归约后的块传给下一个卡
N-1 轮后,所有卡都有完整结果
通信量:2(N-1)/N × 数据大小 ≈ 2×数据大小(与卡数无关!)
3. 通信量对比
| 原语 | 每张卡发送量 | 每张卡接收量 | 总通信量 |
|---|---|---|---|
| all-reduce | ~2×数据 | ~2×数据 | ~2N×数据 |
| all-gather | ~数据 | ~数据 | ~N×数据 |
| reduce-scatter | ~数据 | ~数据 | ~N×数据 |
4. 在实际并行策略中的应用
DDP 训练一步:
1. 每张卡算本地梯度 g_i
2. all-reduce(g_1, ..., g_N) → 每张卡得到平均梯度 ḡ
3. 各自更新参数
FSDP 前向:
1. 每张卡只有参数分片 p_i
2. all-gather(p_1, ..., p_N) → 每张卡得到完整参数 p
3. 做前向计算
FSDP 反向:
1. 每张卡算本地梯度 g_i
2. reduce-scatter(g_1, ..., g_N) → 每张卡只得到自己负责的那块梯度
3. 各自更新参数分片
5. 面试官常见深挖追问
- "为什么 all-reduce 的通信量和卡数无关?"
- 答:因为 Ring All-Reduce 算法中,每张卡只和相邻两张卡通信。数据像接力棒一样在环中传递,N 张卡只需 2(N-1) 步,每步传输 1/N 的数据。总通信量 = 2(N-1)/N × 数据大小 × N = 2(N-1) × 数据大小 ≈ 2×数据大小(常数)。
- "all-reduce 和 reduce-scatter + all-gather 有什么区别?"
- 答:all-reduce = reduce-scatter + all-gather。reduce-scatter 把数据归约后分片发给各卡;all-gather 把各卡的分片收集成完整数据。FSDP 用 reduce-scatter 代替 all-reduce 做梯度同步,因为 FSDP 的参数本身就是分片的,不需要每张卡都有完整梯度。
- "如果网络带宽是 100GB/s,all-reduce 一个 1GB 的梯度需要多久?"
- 答:Ring all-reduce 通信量 ≈ 2 × 1GB = 2GB。时间 = 2GB / 100GB/s = 0.02s = 20ms。这是理论值,实际中还要加上延迟(latency),如 8 卡 NVLink 的实际时间约 5-10ms。
#29. FlashAttention 为什么重要?它到底优化了什么?
#标准答案
FlashAttention 重要,不是因为它改变了 attention 的数学公式,而是因为它优化了 attention 的实现方式,尤其是显存访问和 IO(输入输出)成本。
标准 attention 的一个大问题是:
- 中间 attention score 矩阵很大;
- 会频繁在 HBM(高带宽显存)和算子之间搬运中间结果;
- 于是大量时间花在访存,而不是算数。
FlashAttention 的核心思想是:
- 分块(tiling)计算;
- 尽量避免显式物化完整 attention matrix;
- 让更多中间计算留在更快的片上存储里完成。
所以它本质上是 IO-aware(面向访存开销优化)的 attention 实现,而不是新的建模方法。
#深度解析
1. 标准 Attention 的 IO 瓶颈
标准 Attention 计算流程(HBM 与 SRAM 间多次搬运):
1. 从 HBM 读 Q, K, V
2. 计算 S = Q @ K^T → 写回 HBM
3. 从 HBM 读 S,计算 P = softmax(S) → 写回 HBM
4. 从 HBM 读 P 和 V,计算 O = P @ V → 写回 HBM
问题:S 和 P 都是 n×n 矩阵!
- n=4096, FP16 → S/P 各 32MB
- n=32768 → S/P 各 2GB
- 每次读写 HBM 耗时 >> 计算时间
2. FlashAttention 的核心创新:Tiling + Online Softmax
FlashAttention 分块计算:
1. 把 Q, K, V 分成小块(tile),能放进 SRAM(如 100KB)
2. 每次只加载一个 Q_tile 和 K_tile 到 SRAM
3. 在 SRAM 内计算局部 attention score
4. 用 online softmax 技术,不需要存完整 S 矩阵
5. 逐步累积输出 O,最后写回 HBM
关键:S 和 P 从不完整写入 HBM!
3. IO 复杂度对比
| 实现 | HBM 访问量 | 瓶颈 |
|---|---|---|
| 标准 Attention | O(n²) | HBM 带宽 |
| FlashAttention | O(n) | 计算(不再是 IO) |
FlashAttention 把 attention 从 memory-bound 变成 compute-bound。
4. FlashAttention-1 vs FlashAttention-2
| 特性 | FlashAttention-1 | FlashAttention-2 |
|---|---|---|
| 核心改进 | 减少 HBM 读写 | 减少 warp 间同步,更好并行 |
| 速度提升 | 2-4x | 再提升 1.5-2x |
| 序列长度 | 支持到 64K | 支持到 128K+ |
| 应用 | 训练加速 | 训练和推理都适用 |
5. 面试官常见深挖追问
- "FlashAttention 的 tiling 大小怎么选?"
- 答:取决于 GPU SRAM 大小(如 A100 的 L2 cache 40MB)。Tile 大小要足够大以利用并行度,但又不能太大以 fit 进 SRAM。FlashAttention 会根据硬件自动计算最优 tile 大小(通常 64×64 或 128×128)。
- "FlashAttention 对推理有用吗?"
- 答:有用,但收益不如训练大。训练时 attention 的 Q/K/V 都是完整的(n×d),IO 瓶颈最严重。推理时 Decode 阶段 Q 只有 1 个 token,计算量已经很小,FlashAttention 的收益有限。但 Prefill 阶段(处理 prompt)仍有明显收益。
- "如果 HBM 带宽无限大,FlashAttention 还有优势吗?"
- 答:几乎没有。FlashAttention 的核心是减少 HBM 访问。如果 HBM 带宽无限,标准 attention 和 FlashAttention 速度会趋同。但现实中 HBM 带宽(1-2 TB/s)远低于计算峰值(300+ TFLOPs),所以 FlashAttention 持续有价值。
#30. 为什么说 FlashAttention 的核心是 IO-aware,而不是 O(n^2) 变没了?
#标准答案
因为它没有把 attention 的理论复杂度从 O(n^2) 彻底变成别的量级,token 两两交互这件事仍然存在。
它真正优化的是:
- 不再把巨大中间矩阵完整写出再读回;
- 而是通过 tile 级计算和在线归一化,把访存次数降下来。
所以标准表达应该是:
FlashAttention优化的是常数项和显存/带宽瓶颈;- 它让 attention 在真实硬件上跑得快得多;
- 但它不是从根本上消灭了二次交互。
#深度解析
1. 复杂度 vs IO 优化
| 维度 | 标准 Attention | FlashAttention |
|---|---|---|
| 计算复杂度 | O(n²d) | O(n²d) |
| IO 复杂度 | O(n²d) | O(nd) |
| 显存占用 | O(n²) | O(n) |
| HBM 访问次数 | 多次读写 S, P | 只读写 Q, K, V, O |
FlashAttention 没有改变计算量,但改变了数据搬运量。
2. 为什么强调"IO-aware"?
GPU 的 FLOPS 增长远快于内存带宽:
- 过去 10 年:FLOPS 增长 ~100×
- 过去 10 年:HBM 带宽增长 ~10×
这意味着:计算不是问题,数据搬运才是瓶颈。
FlashAttention 的核心洞察:优化内存访问模式比优化计算更重要。
3. 面试官常见深挖追问
- "FlashAttention 对推理 decode 阶段有用吗?"
- 答:收益有限。Decode 阶段 Q 只有 1 个 token,attention 计算量已经很小,IO 不是瓶颈。但 Prefill 阶段(处理输入 prompt)仍有明显收益,因为此时 Q 是完整的 prompt。
#31. PagedAttention 想解决什么问题?
#标准答案
PagedAttention 主要想解决的是推理时 KV cache 管理效率低、内存碎片严重的问题。
在大模型服务里,不同请求长度不同、生成时长不同,如果简单用连续内存去存每个请求的 KV cache,很容易出现:
- 内存碎片;
- 频繁搬移;
- batch 调度不灵活。
PagedAttention 借鉴了“分页”的思路,把 KV cache 拆成更小的 block/page 来管理,让动态请求更容易复用和调度。
所以它不是在改模型,而是在改推理内存管理策略。
#深度解析
1. 为什么连续内存分配效率低?
传统连续内存分配的问题:
请求 A (seq=100) → 分配 100 个 token 的 KV cache
请求 B (seq=50) → 分配 50 个 token 的 KV cache
请求 C (seq=200) → 分配 200 个 token 的 KV cache
内存布局: [AAAA...][BBB...][CCCCCC...]
问题 1 - 外部碎片:
A 完成后释放: [____...][BBB...][CCCCCC...]
新请求 D (seq=80) 无法放入 A 的空位(80 > 50 但 < 100,有浪费)
问题 2 - 内部碎片:
预分配 max_seq_len=2048,但平均只生成 100 token
每个请求浪费 1948 token 的显存
问题 3 - 无法共享:
两个请求有相同前缀,但 KV cache 各自独立存储
2. PagedAttention 的核心设计
借鉴操作系统的虚拟内存分页:
- 把 KV cache 分成固定大小的 block(如 16 tokens/block)
- 每个请求的 KV cache 是一个 block 列表(不必连续)
- block 按需分配,用完即回收
内存布局:
Block 0: [A0-A15]
Block 1: [A16-A31, B0-B15] ← 共享!
Block 2: [B16-B31]
Block 3: [C0-C15]
请求 A: Block Table = [0, 1]
请求 B: Block Table = [1, 2] ← 和 A 共享 Block 1
请求 C: Block Table = [3, ...]
3. PagedAttention 的收益
| 问题 | 传统连续分配 | PagedAttention |
|---|---|---|
| 外部碎片 | 严重 | 消除(block 统一大小) |
| 内部碎片 | 严重(预分配最大值) | 轻微(按需分配 block) |
| 前缀共享 | 不支持 | 支持(copy-on-write) |
| 动态调度 | 困难(需搬移内存) | 容易(只需改 block table) |
| 抢占/恢复 | 需复制大量内存 | 只需换出 block |
4. Block Size 的选择
| Block Size | 优点 | 缺点 |
|---|---|---|
| 小 (4-8) | 内部碎片少 | Block table 大,管理开销高 |
| 中 (16) | 平衡 | 平衡 |
| 大 (64-128) | 管理简单 | 内部碎片多 |
vLLM 默认 block_size=16,是一个经验折中。
5. 面试官常见深挖追问
- "PagedAttention 和 Continuous Batching 有什么关系?"
- 答:两者配合使用效果最佳。Continuous batching 允许请求动态进出,PagedAttention 提供动态的内存分配/回收。没有 PagedAttention,continuous batching 需要频繁搬移 KV cache(因为新请求插入时,现有请求的缓存可能需要移动以维持连续性)。有了 PagedAttention,只需分配新的 block 给新请求,无需搬移已有数据。
- "前缀共享(Prefix Sharing)怎么实现?"
- 答:当两个请求有相同前缀(如相同的 system prompt),它们的 KV cache 可以共享相同的 block。vLLM 使用 copy-on-write:初始时两个请求指向同一组 block;当某个请求生成新 token 时,才复制该 block(只复制被修改的 block,不是整个序列)。这大幅减少了长 prompt 的显存占用。
- "PagedAttention 的 block table 存在哪里?"
- 答:block table 是轻量级的元数据结构(每个请求一个整数数组),存储在 CPU 内存中。GPU 上只存实际的 KV cache block。调度器在 CPU 上维护 block table,决定每个请求用哪些 block。这种分离让调度逻辑简单高效。
#32. continuous batching 为什么重要?
#标准答案
因为在线推理服务里的请求不是同一时刻整齐到达的,如果必须等一整批凑齐再跑,会浪费很多吞吐,还会增加延迟。
continuous batching 的思路是:
- 允许新请求持续插入正在运行的 batch;
- 已完成的请求可以及时退出;
- 调度器动态维护活跃请求集合。
它重要,是因为它能同时改善:
- GPU 利用率;
- 吞吐;
- 平均等待时间。
但代价是调度逻辑更复杂,和 KV cache 管理、page/block 分配高度耦合。
#深度解析
1. Static Batching 的浪费量化
| 请求 | 输入长度 | 输出长度 | 在 static batch 中的实际计算 |
|---|---|---|---|
| A | 10 | 20 | 必须等 B/C/D 全部完成(假设 max=100) |
| B | 50 | 80 | 同上 |
| C | 200 | 100 | 决定整批完成时间 |
| D | 30 | 40 | 同上 |
假设 4 个请求到达:
关键浪费:A 和 D 在生成 20/40 个 token 后就完成了,但它们的 GPU slot 不能被新请求使用,直到 C 的 100 个 token 全部生成完。这段时间内,A 和 D 的 slot 处于"空转"状态。
2. Continuous Batching 的收益量化
| 指标 | Static Batching | Continuous Batching | 提升 |
|---|---|---|---|
| GPU 利用率 | 40-60% | 80-95% | +50-70% |
| 平均延迟 | 高(等整批完成) | 低(请求随时进出) | -30-50% |
| 吞吐 (tokens/s) | 低 | 高 | +40-80% |
| 尾延迟 (P99) | 很高 | 可控 | 显著改善 |
3. Continuous Batching 的核心机制
时间线:
T=0: Batch = [A(prefill), B(prefill)]
T=1: Batch = [A(decode_1), B(decode_1)]
T=2: Batch = [A(decode_2), B(decode_1), C(prefill)] ← C 插入
T=3: Batch = [A(decode_3), B(decode_2), C(decode_1)]
T=4: Batch = [B(decode_3), C(decode_2)] ← A 完成,退出
T=5: Batch = [B(decode_4), C(decode_3), D(prefill)] ← D 插入
关键设计:
- 每个 forward step 前,调度器检查哪些请求已完成或新到达
- 新请求做 prefill,老请求做 decode,可以在同一个 batch 中混合
- 已完成的请求释放 KV cache slot,立即给新请求使用
4. 与 Dynamic Batching 的区别
| 特性 | Dynamic Batching | Continuous Batching |
|---|---|---|
| batch 组成时机 | 推理前静态组合 | 推理中动态调整 |
| 请求中途加入 | 不支持 | 支持 |
| 请求中途退出 | 不支持 | 支持 |
| 实现复杂度 | 低 | 高 |
| 效果提升 | 中等(10-20%) | 高(40-80%) |
5. 面试官常见深挖追问
- "continuous batching 中,Prefill 和 Decode 混合会不会有问题?"
- 答:不会,但调度需要小心。Prefill 是大矩阵计算(compute-bound),Decode 是小矩阵+KV cache读取(memory-bound)。两者混合时,Prefill 的计算可以"掩盖"Decode 的内存等待,反而提升整体利用率。但如果一个超长 prefill(如 10K tokens)和多个短 decode 混在一起,长 prefill 会阻塞短 decode。解决方案:设置 prefill 长度上限,超长请求拆分或走独立队列。
- "continuous batching 的调度器需要解决哪些核心问题?"
- 答:1)内存管理:动态分配/释放 KV cache(PagedAttention 的 block 机制);2)优先级处理:支持抢占、配额、超时;3)长度差异:短请求不被长请求拖慢(head-of-line blocking);4)Prefill-Decode 混合:同一 batch 中不同阶段的 attention mask 构造。
- "vLLM 的 continuous batching 和 TGI 的有什么区别?"
- 答:核心区别在 KV cache 管理。vLLM 用 PagedAttention(按 block 分配,非连续内存),支持更灵活的内存复用和抢占。TGI 早期用连续内存分配,后来也支持了类似的 page 机制。两者都实现了 in-flight batching(continuous batching 的一种),但 vLLM 的内存效率通常更高。
#33. speculative decoding(投机解码)为什么有效?什么时候不划算?
#标准答案
speculative decoding 通常会让一个小模型先草拟多个 token,再让大模型并行验证。如果草拟大部分都通过,就能减少大模型逐 token 自回归的串行开销。
它有效的前提是:
- 小模型足够快;
- 小模型提议和大模型判断足够一致;
- 验证成本低于完全自己生成。
它不划算的情况通常是:
- 小模型质量太差,命中率低;
- 任务分布复杂,大模型频繁拒绝草稿;
- 系统额外维护两套模型和调度链路,复杂度过高。
所以它不是白捡加速,而是用”双模型协作”换单模型串行瓶颈的缓解。
#深度解析
1. Speculative Decoding 的数学原理:Rejection Sampling
核心思想:小模型(draft model)生成候选序列,大模型(target model)并行验证。
验证过程(对第 i 个候选 token):
1. 大模型计算该位置的真实分布 p(x)
2. 小模型的提议分布 q(x)
3. 计算接受概率: α = min(1, p(x) / q(x))
4. 以概率 α 接受该 token,否则从 (p(x) - q(x))⁺ 重采样
关键性质:只要按上述规则接受/拒绝,最终输出分布严格等于大模型自己的分布。这不是近似,是精确等价。
2. 为什么能加速?
标准解码(大模型):
Step 1: 生成 token_1
Step 2: 生成 token_2
Step 3: 生成 token_3
...(串行,每步一次 forward)
Speculative Decoding:
Step 1: 小模型草稿 [token_1, token_2, token_3, token_4, token_5]
Step 2: 大模型并行验证 5 个 token(一次 forward!)
→ 接受前 3 个,拒绝第 4 个
Step 3: 从第 4 个位置重新草稿
假设:
- 小模型速度是大模型的 5 倍
- 平均接受率 60%
- 每次草稿 5 个 token
则每步平均生成: 5 × 0.6 = 3 个 token(大模型只做了 1 次 forward) 有效加速比: ~2-3 倍
3. 什么时候不划算?
| 条件 | 原因 | 建议 |
|---|---|---|
| 小模型质量太差 | 接受率 <30%,频繁回退 | 换更强的小模型或不用 |
| 任务分布复杂 | 代码/数学任务,大模型经常纠正小模型 | 仅用于简单文本生成 |
| 维护成本高 | 需要同时加载两个模型 | 用 Medusa/EAGLE(单模型多头) |
| 首 token 延迟敏感 | speculative 增加首 token 时间 | 短请求不适合 |
4. 进阶方法:Medusa / EAGLE
| 方法 | 原理 | 优势 | 劣势 |
|---|---|---|---|
| Speculative | 独立小模型草稿 | 通用性强 | 需维护两个模型 |
| Medusa | 在大模型上加多个解码头 | 单模型,无额外加载 | 需要训练新头 |
| EAGLE | 用 attention 特征做草稿 | 接受率更高 | 架构改动大 |
5. 面试官常见深挖追问
- ”speculative decoding 为什么能保证输出分布和大模型完全一致?”
- 答:因为使用了 rejection sampling。当小模型的提议概率 q(x) 小于大模型的真实概率 p(x) 时,一定接受;当 q(x) > p(x) 时,以 p/q 的概率接受。这保证了每个被接受的 token 服从 p(x)。被拒绝时,从修正后的分布重采样。数学上可证明,最终序列的联合分布等于大模型自回归生成的分布。
- ”小模型应该选多大?和 target 模型有什么关系?”
- 答:经验法则:小模型参数量是 target 的 1/10 到 1/5。太小 → 接受率低;太大 → 草稿本身变慢,抵消加速收益。理想情况:小模型和 target 模型同族(如 LLaMA-70B 配 LLaMA-7B),分布相近,接受率高。
- ”为什么代码生成任务用 speculative decoding 效果一般?”
- 答:代码生成对 token 精确度要求极高(一个括号错误就编译失败)。小模型在代码语法上的错误率较高,导致大模型频繁拒绝草稿。接受率低时,speculative 的加速效果被验证开销抵消。解决方案:用代码专用的小模型做草稿,或用 EAGLE 这类特征级草稿方法。
#34. 算子融合(operator fusion)为什么重要?
#标准答案
很多深度学习算子本身计算不算特别大,但如果每一步都单独 launch 一个 kernel,就会有大量 kernel launch 开销和中间结果读写开销。
算子融合的价值就是把多个相邻操作合并成更少的 kernel,例如:
- bias + add + gelu
- layernorm + residual
这样做的好处是:
- 减少 kernel launch 开销;
- 减少中间张量写回显存再读出;
- 提升整体访存效率。
一句话总结:算子融合很多时候不是让数学变少,而是让“搬运和调度”变少。
#深度解析
1. GPU 执行模型的开销构成
单次 kernel 执行时间 = kernel_launch_time + compute_time + memory_access_time
对于小算子:
- kernel_launch_time: ~5-10 μs
- compute_time: ~1-5 μs
- memory_access_time: ~5-20 μs
问题:launch 和访存可能占 80% 时间,实际计算只占 20%!
2. 典型融合场景
| 原始操作 | 融合后 | 效果 |
|---|---|---|
| Linear + Bias + Add + GELU | 单个 fused kernel | 省 3 次 HBM 读写 |
| LayerNorm + Residual + Dropout | 单个 fused kernel | 省 2 次 HBM 读写 |
| Attention QKV 投影 | 合并为单个 gemm | 省 2 次 launch |
| Bias + Add + Activation | fused MLP block | 经典 transformer 优化 |
3. 为什么中间结果读写这么贵?
HBM 带宽 ~1-2 TB/s
SRAM/寄存器带宽 ~10-100 TB/s
差距:10-100 倍!
每次写回 HBM 再读回:
- 写:d bytes → 耗时 d / HBM_bandwidth
- 读:d bytes → 耗时 d / HBM_bandwidth
- 总计:2d / HBM_bandwidth
融合后:
- 中间结果留在 SRAM/寄存器
- 直接传给下一个操作
- 省掉 2d / HBM_bandwidth 的延迟
4. 实际加速效果
| 模型 | 未融合 (ms) | 融合后 (ms) | 加速比 |
|---|---|---|---|
| BERT-Base 推理 | 12.5 | 8.2 | 1.5x |
| GPT-2 训练 step | 185 | 120 | 1.5x |
| LLaMA-7B 推理 | 45 | 32 | 1.4x |
5. 面试官常见深挖追问
- "算子融合和 FlashAttention 有什么关系?"
- 答:FlashAttention 本质上是一种特殊的算子融合。它把
Q@K^T → softmax → @V三个操作融合成一个 kernel,中间结果(attention score)从不写出 HBM。算子融合是通用技术(适用于各种算子组合),FlashAttention 是针对 attention 的专用融合优化。
- 答:FlashAttention 本质上是一种特殊的算子融合。它把
- "PyTorch 怎么自动做算子融合?"
- 答:PyTorch 有多个层次的融合:1)Eager mode:基本不自动融合;2)TorchScript:做一些简单融合;3)TorchInductor(PyTorch 2.0):编译时自动识别并融合相邻算子;4)Triton:手写融合 kernel。生产环境通常用 TorchInductor 或手写 Triton kernel。
- "所有相邻算子都能融合吗?有什么限制?"
- 答:不能。限制包括:1)数据依赖:后一个算子需要前一个算子的完整输出(如 reshape 后不能融合);2)内存布局:不同算子要求的 tensor layout 不同;3)精度要求:某些融合可能引入数值误差(如 LayerNorm 的 mean/var 计算)。编译器(如 XLA、TVM)会做合法性检查。
#35. 如果被问“显存优化该怎么排优先级”,标准回答是什么?
#标准答案
一个稳妥的优先顺序通常是:
- 先分清是哪类显存:参数、优化器状态、激活、
KV cache,不能混着讲; - 训练场景优先考虑:混合精度、
ZeRO/FSDP、activation checkpointing、梯度累积、序列长度控制; - 推理场景优先考虑:量化、
KV cache优化、GQA/MQA、PagedAttention、batch 调度; - 如果还不够,再考虑模型结构层面的改变,例如更小模型、MoE、路由、蒸馏。
面试里最忌讳的是一上来就说“量化一下就好”,因为训练显存和推理显存很多时候不是同一个瓶颈。
#深度解析
1. 显存占用的完整拆解
训练显存 = 模型权重 + 优化器状态 + 梯度 + 激活值
推理显存 = 模型权重 + KV cache
模型权重: 2P (FP16) 或 P (INT8)
优化器状态 (Adam): 8P-12P (FP32 momentum + variance)
梯度: 2P (FP16)
激活值: 取决于 batch_size × seq_len × layers × hidden_dim
KV cache: 2 × batch × seq_len × layers × kv_heads × head_dim × precision
以 7B 模型为例:
| 组件 | 参数量 | FP16 显存 | 占比 |
|---|---|---|---|
| 模型权重 | 7B | 14 GB | ~12% |
| 优化器状态 | 7B | 56 GB | ~48% |
| 梯度 | 7B | 14 GB | ~12% |
| 激活值 | - | 30+ GB | ~28% |
| 训练总计 | - | ~114 GB | - |
关键发现:优化器状态占训练显存的近一半!这就是为什么 ZeRO/FSDP 的核心是分片优化器状态。
2. 训练场景优化优先级(从易到难,从收益大到小)
第一层:立即见效,无效果损失
├─ 混合精度 (AMP): 权重 FP16 + 计算 FP16 → 省 50% 显存
├─ 梯度累积: 小 batch 模拟大 batch → 省激活值显存
└─ 序列截断: 控制 max_seq_len → 线性减少激活值
第二层:有轻微代价,但收益大
├─ ZeRO-1/2/3 / FSDP: 分片优化器状态/梯度/参数
├─ Activation Checkpointing: 用计算换显存(省 30-70% 激活值)
└─ GQA/MQA: 推理时减少 KV cache
第三层:结构级改动,代价明显
├─ 量化训练 (QLoRA): 4-bit 权重 + LoRA
├─ 模型压缩: 剪枝、蒸馏
└─ 序列并行: 把长序列切到多卡
3. 推理场景优化优先级
第一层:收益最大
├─ KV cache 优化: GQA/MQA → 省 4-8 倍
├─ Cache 量化: INT8/INT4 KV cache → 再省 2-4 倍
└─ PagedAttention: 动态分配,减少碎片
第二层:效果显著
├─ 模型量化: INT8/INT4 权重 → 省 2-4 倍
├─ Continuous Batching: 提升吞吐 40-80%
└─ 投机解码: 延迟降低 2-3 倍
第三层:极致优化
├─ 算子融合: 减少 kernel launch 开销
├─ 自定义 CUDA: 针对特定算子优化
└─ 模型蒸馏: 用小模型替代
4. 常见错误:把训练优化和推理优化混为一谈
| 误区 | 真相 |
|---|---|
| "训练时用量化" | 训练时量化(QAT)极不稳定,通常只用于推理 |
| "KV cache 优化能省训练显存" | KV cache 只在推理时存在,训练时优化的是激活值 |
| "activation checkpointing 对推理有用" | 只用于训练,推理不需要保存激活值 |
5. 面试官常见深挖追问
- "如果训练 70B 模型,8×A100 (80GB) 还是 OOM,怎么办?"
- 答:逐步升级:1)ZeRO-3 / FSDP:把参数、梯度、优化器状态分片到 8 卡 → 每卡 ~10GB;2)Activation Checkpointing:激活值省 50-70%;3)Gradient Accumulation:batch_size 降到 1,累积多步;4)Sequence Parallel:把长序列切到多卡;5)如果还不行,加卡或用 8-bit 优化器。
- "为什么优化器状态比模型权重还占显存?"
- 答:Adam 优化器每个参数存:1)当前 FP16 权重(2 bytes);2)FP32 一阶矩 m(4 bytes);3)FP32 二阶矩 v(4 bytes)。所以每个参数的优化器状态是 8 bytes,而权重只有 2 bytes。ZeRO-1 把优化器状态分片到多卡,是解决这个问题的关键。
- "推理时 7B 模型 INT4 量化后,显存能降到多少?"
- 答:权重:7B × 0.5 bytes = 3.5 GB。KV cache(GQA, batch=1, seq=4K):~0.5 GB。总计 ~4 GB。可以在单卡 4090 (24GB) 上轻松运行,甚至同时服务多个请求。
#36. 如果被问“训练系统排障顺序是什么”,标准回答是什么?
#标准答案
最稳的排障顺序通常是:
- 先看现象归类:是 loss 异常、吞吐下降、OOM、Hang 住、NCCL 超时,还是只在特定规模复现;
- 再区分是计算问题还是通信问题:GPU 利用率、网络带宽、step time breakdown 要先看;
- 再定位到层次:数据加载、前向、反向、optimizer step、通信同步、checkpoint 保存分别看;
- 再看最近变更:并行策略、batch size、bucket size、mixed precision、kernel 版本、驱动环境;
- 最后做最小复现:缩 batch、缩卡数、关 checkpointing、关 overlap,一项一项回退。
一句话总结:训练系统排障不能靠猜,必须先做分层,再做最小化隔离。
#深度解析
1. 训练系统故障的分层树
故障现象
├─ Loss 异常
│ ├─ NaN/Inf → 梯度爆炸、数值溢出、数据问题
│ ├─ 不收敛 → 学习率太大、数据质量差、bug
│ └─ 收敛但效果差 → 数据分布、模型容量、任务难度
├─ 吞吐下降
│ ├─ GPU 利用率低 → 数据加载慢、通信阻塞、CPU 瓶颈
│ ├─ step time 变长 → 通信增加、checkpoint、GC
│ └─ 扩展效率低 → 负载不均、通信没重叠
├─ OOM
│ ├─ 激活值太大 → 减 batch、activation checkpointing
│ ├─ 优化器状态 → ZeRO/FSDP
│ └─ 模型权重 → TP/PP/量化
├─ Hang/NCCL 超时
│ ├─ 死锁 → 通信顺序不一致
│ ├─ 某卡挂了 → 硬件故障、散热
│ └─ 网络抖动 → IB/以太网不稳定
└─ 只在特定规模复现
├─ 小 batch 正常,大 batch OOM → 显存问题
├─ 单卡正常,多卡异常 → 通信/同步问题
└─ 小数正常,大数 NaN → 精度/数值稳定性
2. Step Time Breakdown 分析
一个训练 step 的时间构成:
step_time = data_load + forward + backward + optimize + comm + others
典型 7B 模型 8×A100:
- data_load: 5-10%
- forward: 25-30%
- backward: 35-40%
- optimize: 5-10%
- comm (all-reduce): 10-20%
- others (checkpoint, logging): 2-5%
如果 comm > 30% → 通信瓶颈,检查并行策略和网络
如果 data_load > 15% → 数据瓶颈,优化 dataloader
3. 最小复现的"二分法"
问题:8 卡训练 OOM
Step 1: 4 卡是否 OOM?
├─ 是 → 模型/数据问题,继续二分
│ Step 2: 2 卡是否 OOM?
│ ├─ 是 → 单卡试试
│ └─ 否 → 可能是通信相关(如 activation checkpointing 在分布式下的 bug)
└─ 否 → 分布式特定问题
Step 2: 检查 ZeRO/FSDP 配置、TP 切分、PP bubble
4. 常用诊断工具
| 工具 | 用途 | 命令 |
|---|---|---|
| nvidia-smi | GPU 利用率、显存、温度 | nvidia-smi dmon |
| PyTorch Profiler | 每层耗时、kernel 时间 | torch.profiler |
| Nsight Systems | 详细 timeline | nsys profile |
| NCCL Tests | 网络带宽测试 | all_reduce_perf |
| dcgm | GPU 健康监控 | dcgmi diag |
5. 面试官常见深挖追问
- "训练到一半 loss 突然变成 NaN,怎么排查?"
- 答:分层排查:1)检查最近变更(学习率、数据、代码);2)打印梯度范数,看哪层爆炸;3)检查输入数据是否有异常值;4)尝试 gradient clipping(如 clip_norm=1.0);5)检查 mixed precision 的 loss scaling 是否下溢;6)单卡复现,排除分布式问题。
- "8 卡训练比单卡慢,怎么定位?"
- 答:先确认"慢"的定义:是 step time 增加还是扩展效率低?然后用 profiler 看 timeline:1)通信时间占比(all-reduce 是否太长);2)是否有负载不均(某些卡 wait 时间长);3)数据加载是否跟不上;4)NCCL 参数是否正确(如
NCCL_IB_DISABLE)。常见根因:batch size 太小、网络配置错误、没有通信重叠。
- 答:先确认"慢"的定义:是 step time 增加还是扩展效率低?然后用 profiler 看 timeline:1)通信时间占比(all-reduce 是否太长);2)是否有负载不均(某些卡 wait 时间长);3)数据加载是否跟不上;4)NCCL 参数是否正确(如
- "训练系统 Hang 了,但 GPU 利用率 100%,怎么回事?"
- 答:可能是死锁。常见原因:1)通信顺序不一致(如卡 0 先 send 后 recv,卡 1 先 recv 后 send);2)某个 rank 提前退出(如数据不均导致某 rank epoch 结束);3)NCCL 内部死锁。排查:用
gdb attach看堆栈,检查是否在ncclAllReduce等函数中卡住。
- 答:可能是死锁。常见原因:1)通信顺序不一致(如卡 0 先 send 后 recv,卡 1 先 recv 后 send);2)某个 rank 提前退出(如数据不均导致某 rank epoch 结束);3)NCCL 内部死锁。排查:用
#21. 为什么现代开源 LLM 经常采用 RoPE + RMSNorm + SwiGLU + GQA 这套组合?
#标准答案
因为这套组合分别对应四个不同目标:
RoPE负责更自然地把相对位置信息编码进 attention;RMSNorm更偏训练稳定性和实现简洁;SwiGLU让FFN表达能力更强;GQA则主要优化推理阶段的KV cache与带宽压力。
它们之所以常一起出现,不是因为谁“理论上绝对最强”,而是因为这是一组在效果、训练稳定性和推理成本之间比较均衡的工程解。
#深度解析
1. 四组件各司其职
| 组件 | 解决的问题 | 替代方案 | 为什么选它 |
|---|---|---|---|
| RoPE | 位置编码 | 绝对位置编码、Alibi | 相对位置天然、外推性好 |
| RMSNorm | 层归一化 | LayerNorm | 更简单、训练稳定、省计算 |
| SwiGLU | FFN 表达能力 | ReLU FFN、GELU FFN | 门控机制、表达能力更强 |
| GQA | 推理 KV cache | MHA、MQA | 效果与效率的折中 |
2. 为什么这套组合成为"事实标准"?
- LLaMA 开源后,社区大量复用其架构
- 后续模型(Qwen、Baichuan、Mistral)在此基础上微调
- 形成了"预训练效果验证 → 社区跟进 → 生态成熟"的正循环
- 不是理论最优,而是工程上最稳妥的选择
3. 面试官常见深挖追问
- "如果只能换其中一个组件,你会换哪个?为什么?"
- 答:取决于场景。如果追求更长上下文,可能换 RoPE 为 Alibi 或改进外推方法;如果追求极致推理速度,可能换 GQA 为 MQA;如果追求训练稳定性,可能换 RMSNorm 为 LayerNorm。但通常不建议单独换,因为这套组合已经过大量验证。
#22. RMSNorm 和 LayerNorm 的差别是什么?
#标准答案
LayerNorm 会做均值中心化和方差归一化;RMSNorm 通常只根据均方根做缩放,不做显式去均值。
RMSNorm 的好处是更简单、计算略省、在现代 LLM 中通常足够稳定;但它不是“严格全面优于” LayerNorm,而是更符合很多大模型训练实践的经验选择。
#深度解析
1. 数学公式对比
LayerNorm:
μ = mean(x)
σ = sqrt(mean((x - μ)²))
y = (x - μ) / σ * γ
RMSNorm:
rms = sqrt(mean(x²))
y = x / rms * γ
RMSNorm 少了一次减均值操作,计算量略省(约 5-10%)。
2. 为什么 LLM 中去均值不是必须的?
- Attention 后的输出通常已经围绕 0 对称
- 后续线性层(W)可以学习偏移:
W·(x - μ) ≈ W·x - W·μ - 因此去均值的效果可以被权重矩阵隐式学习
3. 效果对比
| 模型 | 使用 Norm | 训练稳定性 |
|---|---|---|
| BERT | LayerNorm | 稳定 |
| GPT-3 | LayerNorm | 稳定 |
| LLaMA | RMSNorm | 稳定 |
| Mistral | RMSNorm | 稳定 |
大量实验表明,在 Decoder-only LLM 上,RMSNorm 与 LayerNorm 效果相当。
4. 面试官常见深挖追问
- "Pre-Norm 和 Post-Norm 有什么区别?为什么现在多用 Pre-Norm?"
- 答:Post-Norm:x + Sublayer(Norm(x)),梯度需要经过 Norm 层,深层时梯度容易消失。Pre-Norm:Norm(x + Sublayer(x)),梯度路径更短,训练更稳定。现代 LLM 几乎都用 Pre-Norm。
#23. SwiGLU 为什么常被认为比普通 FFN 更好?
#标准答案
因为它不是简单线性变换后接固定激活,而是通过门控机制让不同通道的信息流更有选择性。直觉上可以理解成:FFN 不只是“放大再压回去”,而是多了一层可学习门控,因此表达能力通常更强。
代价是参数和计算会比最朴素的两层 ReLU FFN 更高一些,但在现代 LLM 中这通常是值得的。
#深度解析
1. SwiGLU 的数学形式
标准 FFN: FFN(x) = max(0, x·W_1 + b_1)·W_2 + b_2
SwiGLU: SwiGLU(x) = (x·W_gate ⊗ Swish(x·W_up))·W_down
其中 Swish(x) = x · sigmoid(βx),通常 β=1
SwiGLU 多了门控矩阵 W_gate,让网络可以学习"哪些通道该通过、哪些该阻断"。
2. 为什么表达能力更强?
| 特性 | ReLU FFN | SwiGLU |
|---|---|---|
| 非线性 | 固定 ReLU | 可学习的门控 |
| 信息筛选 | 无(所有通道统一处理) | 有(门控选择通道) |
| 参数量 | 2·d·d_ff | 3·d·d_ff(多一个 gate 矩阵) |
3. 面试官常见深挖追问
- "SwiGLU 比普通 FFN 多 50% 参数,值得吗?"
- 答:通常值得。实验表明 SwiGLU 在相同总参数量下效果优于标准 FFN。即使参数量多 50%,训练稳定性通常更好,收敛更快。现代 LLM 普遍采用 SwiGLU(LLaMA、Mistral、Qwen 等)。
#24. MLA、MQA、GQA 的共同主题是什么?
#标准答案
它们共同指向一个核心问题:推理阶段 attention 的缓存和带宽成本太高。
MQA通过更多 query 头共享同一组K/V来极限压缩缓存;GQA通过分组共享做折中;MLA则更进一步,试图通过潜变量或更紧凑表示进一步压缩缓存与带宽。
所以它们都是“为长上下文推理服务”的路线,而不是单纯为了增加模型参数量。
#深度解析
1. Attention 推理的显存瓶颈
标准 MHA 的 KV Cache:
每层: 2 × n_heads × seq_len × head_dim × dtype
32 层 × 32 heads × 4096 × 128 × 2 bytes = 2 GB (batch=1)
当 seq_len 从 4K → 128K:
- KV Cache 从 2GB → 64GB
- 单卡无法容纳
2. 三种方法的定量对比
| 方法 | KV heads | 4K 缓存 | 128K 缓存 | 效果损失 |
|---|---|---|---|---|
| MHA | 32 | 2 GB | 64 GB | 0% |
| GQA | 8 | 0.5 GB | 16 GB | <1% |
| MQA | 1 | 0.06 GB | 2 GB | 2-3% |
| MLA (DeepSeek) | 压缩表示 | ~0.1 GB | ~3 GB | ~0% |
3. MLA 的创新
MLA (Multi-head Latent Attention):
- 不直接缓存 K/V,而是缓存低维 latent 向量
- 需要时从 latent 恢复 K/V
- 压缩比可达 10-20×
- 代表:DeepSeek-V2
4. 面试官常见深挖追问
- "GQA 为什么效果损失比 MQA 小?"
- 答:GQA 保留了多组 K/V(如 8 组),每组服务 4 个 query head。这样不同 query head 仍有一定选择空间,信息多样性比 MQA(只有 1 组)好得多。实验表明 GQA(8) 与 MHA 的差距通常 <1%,而 MQA 差距 2-3%。
#25. FlashAttention 为什么重要?
#标准答案
FlashAttention 重要,不是因为它改变了 Transformer 的数学定义,而是因为它显著优化了 attention 的实际执行方式。它通过更好的分块和内存访问策略,避免显式保存大中间矩阵,降低 IO 压力,因此常常能带来更快速度和更低显存占用。
一句话总结:它更像“把同一个 attention 算法跑得更对、更快”。
#深度解析
1. FlashAttention 解决了什么问题?
标准 Attention 的痛点:
- 序列长度 L=4K 时,Attention Score 矩阵大小 = 4096² × 32 heads × 4 bytes ≈ 2 GB
- 标准实现需要多次读写这个 2GB 矩阵到 HBM
- HBM 带宽 (~1.5TB/s) 成为瓶颈,GPU 计算单元大量空闲
FlashAttention 的解决:
- 将 Q/K/V 分块加载到 SRAM (速度快 10×+)
- 在 SRAM 内完成整个 attention 计算
- 中间结果(S, P 矩阵)不写入 HBM
- IO 从 O(L²) 降到 O(L)
2. 实际收益
| 指标 | 标准 Attention | FlashAttention | 提升 |
|---|---|---|---|
| 速度 (L=4K) | 1× | 2-3× | 2-3× |
| 速度 (L=16K) | 1× | 4-6× | 4-6× |
| 显存 (L=16K) | OOM | 正常运行 | 避免 OOM |
| 数值精度 | 可能累积误差 | 更精确 | 更稳定 |
3. 为什么不是改变数学定义?
FlashAttention 的数学等价于标准 Attention:
标准: O = softmax(QK^T/√d)V
Flash: O = softmax(QK^T/√d)V (结果完全相同)
区别仅在于计算顺序和内存访问模式,不是近似算法。
4. 面试官常见深挖追问
- "FlashAttention 的 backward 为什么也需要重算 forward?"
- 答:因为 forward 的中间结果(S, P)没有保存。backward 需要 P 来计算 dL/dQ, dL/dK, dL/dV,所以必须重算 forward。代价是 backward 时间约为 forward 的 2-2.5 倍(标准实现也是约 2 倍,所以 overhead 不大)。
#26. 为什么 Mamba/SSM 很受关注,但还没有完全替代 Transformer?
#标准答案
因为它们瞄准的是 Transformer 最痛的点:长序列下的二次复杂度与缓存压力。Mamba/SSM 这类方法通常强调线性复杂度、状态压缩和长序列效率。
但现实世界里,一个架构能否成为主流,不只看理论复杂度,还看:
- 训练是否稳定;
- 是否容易扩展到超大规模;
- 推理生态是否成熟;
- 社区和工具链是否跟得上。
所以现在更准确的说法是:它们是重要方向,但尚未完成对 Transformer 的全面替代。
#深度解析
1. Transformer vs SSM 复杂度对比
| 维度 | Transformer | SSM (Mamba) |
|---|---|---|
| 训练复杂度 | O(L²) | O(L) |
| 推理复杂度 | O(L) per step | O(1) per step |
| 状态大小 | O(L) (KV Cache) | O(1) (固定状态) |
| 长序列能力 | 受限于显存 | 天然支持 |
| 训练稳定性 | 成熟 | 仍在优化 |
| 生态成熟度 | 极成熟 | 初期 |
2. 为什么还没替代?
| 障碍 | 说明 |
|---|---|
| 训练稳定性 | SSM 的状态转移矩阵需要精心初始化,深层时容易不稳定 |
| Scaling 验证 | Transformer 已在 100B+ 规模验证,SSM 还缺乏同规模证据 |
| 选择性机制 | Mamba 的输入依赖状态转移是重要创新,但增加了复杂度 |
| 工具链 | FlashAttention、vLLM 等成熟工具都是为 Transformer 设计 |
3. 面试官常见深挖追问
- "SSM 的 O(L) 复杂度是指什么?"
- 答:SSM 的状态更新是递推的:
h_t = A·h_{t-1} + B·x_t,每步只依赖前一步状态。因此处理长度为 L 的序列只需 O(L) 步,每步 O(1)。而 Transformer 的 Attention 需要计算所有 token 两两之间的关系,是 O(L²)。
- 答:SSM 的状态更新是递推的:
#27. KV cache 的大小如何快速估算?
#标准答案
粗略公式是:2 * B * T * L * n_kv_heads * head_dim * dtype_bytes。
重点要会解释每一项的含义,而不是死背公式。面试官真正想看的是你能否从式子里立刻读出:为什么 batch、上下文长度、层数和 kv_heads 一上去,推理显存会很快爆炸。
#深度解析
1. 公式各项含义
KV Cache = 2 × B × T × L × n_kv_heads × head_dim × dtype_bytes
2: K 和 V 两个张量
B: batch size (并发请求数)
T: 序列长度 (上下文长度)
L: Transformer 层数
n_kv_heads: KV 头数 (MHA=全部, GQA=分组, MQA=1)
head_dim: 每个头的维度 (如 128)
dtype_bytes: 数据类型大小 (FP16=2, FP8=1, INT8=1)
2. 快速估算示例
以 LLaMA-2 7B (GQA) 为例:
B=1, T=4096, L=32, n_kv_heads=8, head_dim=128, FP16
KV Cache = 2 × 1 × 4096 × 32 × 8 × 128 × 2
= 536,870,912 bytes
≈ 512 MB
如果 T=32K: 512 MB × 8 = 4 GB
如果 B=8: 4 GB × 8 = 32 GB
3. 面试官常见深挖追问
- "如果 batch_size 翻倍,KV Cache 怎么变?"
- 答:线性翻倍。每个请求独立维护自己的 KV Cache。但要注意:如果请求共享前缀(如 system prompt),可以用 PagedAttention 的 copy-on-write 机制共享,避免重复存储。
#28. 如果面试官让你手撕 RoPE 或 LoRA Linear,他真正想看什么?
#标准答案
不是看你记没记住某个库的 API,而是看:
- 你是否知道
shape如何流动; - 你是否真的理解
RoPE是对Q/K做旋转,而不是对 token 直接加位置; - 你是否知道
LoRA是冻结 base weight,只训练低秩增量; - 你能不能把概念还原为一个最小可执行模块。
#深度解析
1. 手撕代码的评分标准
| 维度 | 优秀 | 及格 | 不及格 |
|---|---|---|---|
| Shape 意识 | 每步标注 tensor shape | 部分标注 | 完全不标注 |
| 数值稳定 | 主动处理溢出/下溢 | 被追问后才处理 | 完全没考虑 |
| API 独立 | 用基础运算实现 | 调用高层 API | 只背 API 名字 |
| 可运行 | 写完能直接跑 | 有小 bug 但能改 | 逻辑错误无法运行 |
2. RoPE 最小实现要点
def apply_rope(q, k, pos_idx, base=10000):
"""
q, k: (batch, heads, seq, dim)
pos_idx: (seq,) 位置索引
"""
# 1. 按维度两两分组
# 2. 对每组应用旋转矩阵
# 3. 旋转角度 = pos * base^(-2i/d)
# 关键点:不是加到 embedding 上,而是旋转 Q/K
3. LoRA Linear 最小实现要点
class LoRALinear(nn.Module):
def __init__(self, in_features, out_features, rank=16):
self.base = nn.Linear(in_features, out_features)
self.base.weight.requires_grad = False # 冻结
self.lora_A = nn.Linear(in_features, rank, bias=False)
self.lora_B = nn.Linear(rank, out_features, bias=False)
# B 初始化为 0,保证训练开始时 ΔW=0
def forward(self, x):
return self.base(x) + self.lora_B(self.lora_A(x))
4. 面试官常见深挖追问
- "手撕时如果忘记某一步,怎么办?"
- 答:诚实地说"这里我记得需要 X,但具体实现细节可能需要查一下"。然后说明 X 的作用和为什么需要。面试官更看重你的概念理解,而不是背诵代码。