如何速成LLM以伪装成一个AI研究者(7)——面试八股【持续更新中】

往期回顾

如何速成LLM以伪装成一个AI研究者(1)——循环,卷积,编解码器,注意力,Transformer
如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
如何速成LLM以伪装成一个AI研究者(3)——预训练,监督微调,强化学习RLHF/DPO
如何速成LLM以伪装成一个AI研究者(4)——PPO,GRPO,DAPO,GSPO
如何速成LLM以伪装成一个AI研究者(5)——显存估算,显卡选择
如何速成LLM以伪装成一个AI研究者(6)——LoRA,Adapter,P-tuning,量化,QLoRA

免责声明:作者也是伪装的,有错漏属于正常现象,欢迎评论指正。

从“伪装”迈向实践

计算机科学是工程学科,人们关心什么问题、为什么关心?有哪些想法看着很美好,实践起来困难重重?哪些公司在无依无据地吹牛逼,谁又在脚踏实地地做实事?这些往往需要积累才能判断。

关于如何入门LLM,有看到一句说得很在理的话:

首先看完Attention is all you need,然后找一份实习。

呃,但是遇到下面的情况怎么办?
请添加图片描述
这不就是伪装的意义吗!

这种伪装倒不是让你骗人,有实践经验的地方你可以阐述得更详细,没有经验的地方可以承认自己实践经验不足,但依然可以阐述自己对相关问题的一定见解,让面试官看到你不是大脑空空。最终,当你真的有了经验,有了自己的research taste,有自己的实践经验和技术价值观判断后,也就不再需要花时间琢磨怎么伪装了。

那么接下来我们来做一些面试测试题(俗称面试八股),也算是“伪装课程”的一个阶段性测试。参考答案是作者的视角答案,不一定是最好的,并且主要集中在RL/Agent方向。作者也会持续性收集一些问题更新博文。

面试八股暨阶段性测试

架构

简述RMSNorm和LayerNorm的区别?

如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
LayerNorm同时调整可学习缩放参数 γ \gamma γ(方差)和可学习平移参数 β \beta β(均值),RMSNorm只调整 γ \gamma γ。RMSNorm 的原始论文通过实验证明了 β \beta β的重要性远不如 γ \gamma γ,所以RMSNorm可以在相似性能的情况下,因为减少可学习参数而计算量更小、更稳定,是现在主流架构的选择。

简述pre-norm(pre-LN)和post-norm(post-LN)的区别?

如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
在训练初期或者数值不稳定时,post-norm的整个链式求导的谱范数可能出现指数级增长,因此刚需warm-up流程。pre-norm对残差连接保留得更好,它的梯度和训练效果更加稳定,是现在的主流选择。

RL

RL训练中一般需要关注哪些指标?

  1. 奖励(Reward):平均奖励及其趋势,判断模型是否在优化目标。
  2. KL 散度(KL Divergence):当前策略与参考模型间的差异,防止偏离过远。
  3. 响应长度(Response Length):生成序列的平均/最大长度,监控是否出现长度崩塌或不合理增长。
  4. 策略熵(Policy Entropy):反映输出的多样性,熵过低易导致重复或退化。

RL训练中,出现重复输出可能是什么原因导致的?

  1. Reward Hacking:模型发现重复某些高奖励片段(如特定格式、关键词)能以低成本骗取更高回报,属于对奖励函数的过度优化。
  2. KL约束失效:KL散度惩罚过弱或系数衰减过快,策略严重偏离原始SFT模型,丧失自然语言多样性。
  3. 熵正则化不足:策略熵过低,采样趋于贪婪,迅速陷入局部最优循环。

RL训练中,出现乱码输出可能是什么原因导致的?

  1. 数据分布问题:预训练/RL训练/测试数据的分布不一致,RL训练数据的词表覆盖率不足,部分token的logprob出现异常。
  2. 训练不稳定:学习率过大、PPO的clip范围过宽或梯度累积不当,导致策略网络权重更新失控。

RL训练为什么会让seq_len变长?

模型为了获取更高奖励,会倾向于生成更详细、更完整的内容(比如添加解释、推理步骤、安全声明等)。如果奖励函数无意中偏好长回答(如避免信息缺失或格式要求),模型就会学到“输出更多 token 能提高得分”,从而导致生成序列长度自动增长。

如何控制entropy?

这里我推荐一篇论文:Addressing Performance Saturation for LLM RL via Precise Entropy Curve Control,对这个问题分析得比较全面。以下是小结:
熵崩塌原因:正样本导致熵降,负样本导致熵增。因为训练后期正样本越来越多,模型熵就崩了,只会输出少数“安全”答案。
控熵办法:损失正则化(加入熵惩罚项),裁剪技术(限制更新幅度),正负样本解耦(分别处理正负样本)
本文方法:使用拒绝采样(Reject Sampling),控制正负样本数量来控熵,初始目标(0.6,0.7),最终目标(0.1,0.2),退火方式线性衰减

GAE是什么? γ \gamma γ一般怎么设置?

如何速成LLM以伪装成一个AI研究者(4)——PPO,GRPO,DAPO,GSPO
GAE是一种平衡优势函数的偏差和方差的技巧。 γ \gamma γ越大,越低偏差、高方差,模型越考虑长期回报。 γ \gamma γ越小,越高偏差、低方差,模型越不考虑长期回报。为了平衡方差和长期回报,常见任务一般取0.95-0.99。

KL估计器是什么?K1、K2、K3估计器有什么区别?

推荐博客:KL散度近似方法介绍:从John Schulman的博客到DeepSeek GRPO的应用
KL估计器是PPO作者John Schulman在其博客中提出的,Monte Carlo估计KL散度的时候减小方差的近似估计方法。其中, K1简单无偏,但方差大,K2有偏,减少方差,K3无偏但是减少方差。实践中多用K3。

正向KL和反向KL是什么?在RL应用中有什么不同?

如何速成LLM以伪装成一个AI研究者(3)——预训练,监督微调,强化学习RLHF/DPO
设真实分布(客观分布)为 P P P,拟合分布(主观分布)为 Q Q Q,正向KL(fKL)为 K L ( P ∥ Q ) = ∑ x P ( x ) log ⁡ P ( x ) Q ( x ) KL(P \parallel Q)=\sum_x P(x)\log \frac{P(x)}{Q(x)} KL(PQ)=xP(x)logQ(x)P(x),反向KL(rKL)为 K L ( Q ∥ P ) = ∑ x Q ( x ) log ⁡ Q ( x ) P ( x ) KL(Q \parallel P)=\sum_x Q(x)\log \frac{Q(x)}{P(x)} KL(QP)=xQ(x)logP(x)Q(x)
fKL是mode-covering(覆盖模式)——会狠狠惩罚 P ( x ) P(x) P(x)大,而 Q Q Q没有覆盖到的地方,因此Q会倾向于把峰铺开。
rKL是mode-seeking(追逐主峰)——关注 Q ( x ) Q(x) Q(x)大的地方,是否和 P P P分布一致。故Q会倾向于抓住P最主要的峰。
fKL 常用于 模仿学习 / 行为克隆,而rKL一般用于策略优化(PPO / TRPO / RLHF),以约束新策略和旧策略的主峰不偏离。

PPO的loss包含哪些?

PPO 的 loss 包含三部分:
策略损失(clipped surrogate):核心项,通过 clip 限制更新幅度。
价值损失(value loss):通常为价值网络输出的均方误差(MSE)。
熵正则项(entropy bonus):鼓励探索,增加策略随机性。

PPO的clip有什么用?

用于限制策略更新的幅度,防止单步更新过大导致训练不稳定或性能崩塌。

Policy的top_P,top_K是什么?

在策略模型(生成)中,top_p 和 top_k 是两种截断采样策略,用于控制输出的随机性与多样性:
top_k:仅从概率最高的 k 个 token 中采样(例如 top_k=50),其余 token 概率置零并重新归一化。可防止低概率 token 被选中。
top_p(核采样):从累积概率和刚好超过 p(如 0.9)的最小 token 集合中采样,动态调整候选集大小。比 top_k 更自适应。
两者常结合使用:先按 top_k 过滤,再按 top_p 截断。

大模型RL训练中,loss初始值为0正常吗?

PPO的情况:不正常。PPO的优势函数一般非0,并且value loss一般非0。
GRPO的情况:正常。
max ⁡ θ J G R P O ( θ ) = E [ 1 G ∑ i = 1 G 1 ∣ o i ∣ ∑ t = 1 ∣ o i ∣ min ⁡ ( r t ( θ ) A ^ i , t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A ^ i , t ) ] \begin{equation} \max_{\theta} J_{GRPO}(\theta)=E[\frac{1}{G}\sum_{i=1}^G \frac{1}{|o_i|}\sum_{t=1}^{|o_i|}\min (r_t(\theta) \hat{A}_{i,t}, clip(r_t(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_{i,t})] \end{equation} θmaxJGRPO(θ)=E[G1i=1Goi1t=1oimin(rt(θ)A^i,t,clip(rt(θ),1ϵ,1+ϵ)A^i,t)]
初始 π θ = π o l d \pi_{\theta}=\pi_{old} πθ=πold,故KL散度项为0,并且 r t ( θ ) = 1 r_t(\theta)=1 rt(θ)=1。而 A ^ i , t \hat{A}_{i,t} A^i,t有做过归一化,在 r t ( θ ) = 1 r_t(\theta)=1 rt(θ)=1的情况下, ∑ A ^ i , t = 0 \sum \hat{A}_{i,t}=0 A^i,t=0。故GRPO的loss为0是正常的。

已经有奖励函数了,为什么强化学习(GRPO)还需要优势呢?

奖励函数给出的是绝对得分,但不同 prompt 的难度不同,奖励尺度难以直接比较。优势函数通过组内标准化将奖励转为相对好坏(均值为0),让模型专注于同一 prompt 下回答间的优劣差异,从而稳定训练、避免跨 prompt 的尺度干扰。

为什么计算优势时要乘以重要性比率( r t ( θ ) r_t(\theta) rt(θ))?

用于纠正数据分布偏移。我们只能从旧策略采样的数据中计算优势,但更新的是新策略。乘以该比率后,损失函数的梯度等价于从新策略采样下的期望梯度,从而允许安全地重复使用旧数据进行多轮更新。

简述GSPO相对GRPO做了什么改进?

GRPO (Token级):每个Token计算一次重要性权重,进而导致权重剧烈波动、方差大。
GSPO (序列级):整个序列计算一次总重要性比率,信号平滑稳定,有效避免了噪声累积。

Agent

Thinking主要提升了agent哪方面的能力?

planning的能力,状态理解和资源规划的能力,反思和纠错能力。

为什么有些时候,Thinking反而破坏了agent的性能?

我认为主要是训练错位问题,思考模型经常在数学题之类的高反思场景训练,没有与agent适配,导致Thinking模型容易内耗(倾向于内部模拟而轻视与外部环境的交互),降低效率。但是“agent不需要数学题一样的thinking”不等于“agent不需要thinking”,我认为thinking依旧可以提升agent的能力上限。

实现长上下文Agent有什么主要挑战?

  1. 链式崩塌与纠错能力:Agent每一步都是概率性的,偏差容易在长期执行中被无限放大。
  2. 长上下文记忆
  3. 长期规划与planning、子任务划分能力
  4. 可观测性和调试:Agent的推理链路不可复现,无法日志打断点。
  5. 成本和延迟控制

multi-agent有哪些编排格式?

Orchestrator-Worker(主从):最常用、最可控、最适合并行
Sequential:分工按序列运行,一般用于处理大量相似事件。容易积累误差。
Hierarchical:对于复杂度较高的任务,可能需要分层管理结构合作
Group Chat:看起来酷,一用一个不吱声

手撕

MHA

import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiHeadAttention(nn.Module):
    """简易多头注意力实现,展示主要功能"""
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"
        self.d_model = d_model      # 输入/输出特征维度
        self.num_heads = num_heads  # 注意力头数
        self.d_k = d_model // num_heads  # 每个头的维度

        # 线性变换层:生成 Q, K, V 和最终输出投影
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def _split_heads(self, x, batch_size):
        """将最后一维拆分成 (num_heads, d_k),并交换维度便于并行计算"""
        # x: (batch, seq_len, d_model) -> (batch, seq_len, num_heads, d_k)
        x = x.view(batch_size, -1, self.num_heads, self.d_k)
        # 转换为 (batch, num_heads, seq_len, d_k)
        return x.transpose(1, 2)

    def _combine_heads(self, x, batch_size):
        """将多头结果合并回 (batch, seq_len, d_model)"""
        # x: (batch, num_heads, seq_len, d_k) -> (batch, seq_len, num_heads, d_k)
        x = x.transpose(1, 2).contiguous()
        # -> (batch, seq_len, d_model)
        return x.view(batch_size, -1, self.d_model)

    def _scaled_dot_product_attention(self, Q, K, V, mask=None):
        """缩放点积注意力核心计算"""
        # Q, K, V: (batch, num_heads, seq_len, d_k)
        scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        attn_weights = F.softmax(scores, dim=-1)   # (batch, num_heads, seq_len, seq_len)
        output = torch.matmul(attn_weights, V)     # (batch, num_heads, seq_len, d_k)
        return output, attn_weights

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)

        # 1. 线性变换得到 Q, K, V
        Q = self.W_q(query)
        K = self.W_k(key)
        V = self.W_v(value)

        # 2. 拆分为多个头
        Q = self._split_heads(Q, batch_size)
        K = self._split_heads(K, batch_size)
        V = self._split_heads(V, batch_size)

        # 3. 缩放点积注意力 (每个头独立计算)
        attn_output, attn_weights = self._scaled_dot_product_attention(Q, K, V, mask)

        # 4. 合并所有头
        concat_output = self._combine_heads(attn_output, batch_size)

        # 5. 最终线性变换
        output = self.W_o(concat_output)
        return output, attn_weights


# ========== 示例:展示 MHA 主要功能 ==========
if __name__ == "__main__":
    # 超参数
    d_model = 512   # 嵌入维度
    num_heads = 8   # 多头数量
    batch_size = 2
    seq_len = 10

    # 模拟输入 (batch, 序列长度, 特征维度)
    query = torch.randn(batch_size, seq_len, d_model)
    key   = torch.randn(batch_size, seq_len, d_model)
    value = torch.randn(batch_size, seq_len, d_model)

    # 创建多头注意力模块
    mha = MultiHeadAttention(d_model, num_heads)

    # 前向传播,得到输出和注意力权重
    output, attn_weights = mha(query, key, value)

    print("输入 query shape:      ", query.shape)          # (2, 10, 512)
    print("输出 shape:            ", output.shape)         # (2, 10, 512)
    print("注意力权重 shape:      ", attn_weights.shape)   # (2, 8, 10, 10)
    print("多头注意力机制运行完毕,每个头关注不同子空间的信息。")

GQA

import torch
import torch.nn as nn
import torch.nn.functional as F

class GroupedQueryAttention(nn.Module):
    """分组查询注意力 (GQA) - 简洁实现"""
    def __init__(self, d_model, num_heads, num_kv_heads):
        super().__init__()
        assert d_model % num_heads == 0
        assert num_heads % num_kv_heads == 0, "查询头数必须是KV头数的整数倍"
        self.d_model = d_model
        self.num_heads = num_heads
        self.num_kv_heads = num_kv_heads
        self.d_k = d_model // num_heads              # 每个头的维度
        self.group_size = num_heads // num_kv_heads  # 每组包含的查询头个数

        # Q, K, V 线性投影 (K和V只投影 num_kv_heads 份)
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, num_kv_heads * self.d_k)
        self.W_v = nn.Linear(d_model, num_kv_heads * self.d_k)
        self.W_o = nn.Linear(d_model, d_model)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)
        seq_len = query.size(1)

        # 1. 线性投影 -> (batch, seq_len, num_heads*d_k) 和 (batch, seq_len, num_kv_heads*d_k)
        Q = self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(key).view(batch_size, -1, self.num_kv_heads, self.d_k).transpose(1, 2)
        V = self.W_v(value).view(batch_size, -1, self.num_kv_heads, self.d_k).transpose(1, 2)

        # 2. GQA 核心:将 K, V 按组重复,使头数与 Q 对齐
        #    形状从 (batch, num_kv_heads, seq_len, d_k) 扩展到 (batch, num_heads, seq_len, d_k)
        K = K.repeat_interleave(self.group_size, dim=1)  # 沿着头维度重复
        V = V.repeat_interleave(self.group_size, dim=1)

        # 3. 缩放点积注意力 (标准计算)
        scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        attn_weights = F.softmax(scores, dim=-1)
        attn_output = torch.matmul(attn_weights, V)      # (batch, num_heads, seq_len, d_k)

        # 4. 合并头并输出投影
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        return self.W_o(attn_output)


# ========== 演示 GQA 主要功能 ==========
if __name__ == "__main__":
    d_model = 512
    num_heads = 8
    num_kv_heads = 2          # 2组KV头,每组服务4个查询头 (group_size=4)
    batch, seq_len = 2, 10

    query = torch.randn(batch, seq_len, d_model)
    key   = torch.randn(batch, seq_len, d_model)
    value = torch.randn(batch, seq_len, d_model)

    gqa = GroupedQueryAttention(d_model, num_heads, num_kv_heads)
    out = gqa(query, key, value)

    print("输入 shape:", query.shape)        # (2, 10, 512)
    print("输出 shape:", out.shape)          # (2, 10, 512)
    print(f"GQA 完成:{num_heads} 个查询头,仅 {num_kv_heads} 个KV头,减少 KV 缓存至原来的 {num_kv_heads/num_heads:.0%}")

GRPO Loss

import torch
import torch.nn.functional as F

def grpo_loss(policy_logps, ref_logps, old_logps, rewards, group_ids,
              epsilon=0.2, beta=0.04):
    """
    GRPO 损失函数 (简化版,核心计算流程)
    
    Args:
        policy_logps: (B,) 当前策略下生成序列的对数概率
        ref_logps:    (B,) 参考模型(通常是冻结的 SFT 模型)的对数概率
        old_logps:    (B,) 旧策略(更新前的策略)的对数概率
        rewards:      (B,) 每个序列的奖励值(原始环境奖励或结果奖励)
        group_ids:    (B,) 每个序列所属的组编号(每组对应同一 prompt 的多个采样)
        epsilon:      PPO clip 范围
        beta:         KL 惩罚系数
    
    Returns:
        损失标量
    """
    # 1. 组内奖励标准化 → 优势 (advantage)
    advantages = torch.zeros_like(rewards)
    for gid in torch.unique(group_ids):
        mask = (group_ids == gid)
        group_rewards = rewards[mask]
        mean = group_rewards.mean()
        std = group_rewards.std() + 1e-8
        advantages[mask] = (group_rewards - mean) / std   # 组内相对优势

    # 2. 概率比 r(θ) = exp(π_θ / π_old)
    ratio = torch.exp(policy_logps - old_logps)

    # 3. PPO clipped 策略损失 (仅针对被采样序列)
    surr1 = ratio * advantages
    surr2 = torch.clamp(ratio, 1 - epsilon, 1 + epsilon) * advantages
    policy_loss = -torch.min(surr1, surr2).mean()

    # 4. KL 散度惩罚 (当前策略 vs 参考策略)
    #    常用近似:KL(π_θ || π_ref) = exp(logπ_ref - logπ_θ) - (logπ_ref - logπ_θ) - 1
    kl_penalty = (torch.exp(ref_logps - policy_logps) - (ref_logps - policy_logps) - 1).mean()

    # 总损失
    loss = policy_loss + beta * kl_penalty
    return loss


# ========== 使用示例 ==========
if __name__ == "__main__":
    # 假设 batch 中有 4 个序列,来自 2 个不同的 prompt(每组 2 个采样)
    batch_size = 4
    policy_logps = torch.randn(batch_size)   # 当前策略对数概率
    ref_logps    = torch.randn(batch_size)   # 参考模型对数概率
    old_logps    = torch.randn(batch_size)   # 旧策略对数概率
    rewards      = torch.tensor([0.8, 0.3, 0.6, 0.9])  # 原始奖励
    group_ids    = torch.tensor([0, 0, 1, 1])          # 组 ID

    loss = grpo_loss(policy_logps, ref_logps, old_logps, rewards, group_ids)
    print(f"GRPO Loss: {loss.item():.4f}")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值