夜雨聆风学习资料网

ARTICLE · 1153133

LLM大厂手撕真题(字节/阿里/百度/美团)

LLM大厂手撕真题(字节/阿里/百度/美团)

✅我是丁师兄,专注于智能驾驶大模型,持续分享LLM面试干货。

✅大模型1v1辅导,已帮助多名同学成功上岸

offer捷报

历经5轮面试,拿下特斯拉offer......

需要大模型1v1辅导的同学,私信我上车。方法+实战+陪跑,带你一步步把offer拿稳。

本文为 LLM 面试的手撕代码环节。主要来源是 leetcode,今年的暑期实习的情况是,手撕的越来越少,手撕集中在一面技术面,二面三面基本不太考手撕。

面了很多家,如果第一轮没有手撕基本上之后就没有手撕了,字节、美团和蚂蚁是不管怎么样肯定会考手撕的,qwen 和腾讯这边没考手撕。

建议对 hot100 的原题和扩展题目都详细的梳理清楚。下面为本人在面试过程中被问到过的题目。

01

题目

(1)

岛屿数量 【字节】

编辑距离【字节】

K 个一组翻转链表【字节 seed】

(2)

第 k 大整数 【美团,美团一面很喜欢考这个题目】

(3)

股票最大收益 【百度文心code】

(4)

最长递增子序列

leetcode 合并 K 个升序链表 【字节】

根据 rand7 实现 rand10 【阿里云】

线性回归(只允许使用 numpy)【minimax】

(5)

使用闭式解进行求解

使用梯度下降进行求解

import numpy as npclass LinearReg:    def __init__(self, dim) -> None:        pass    def fit(self, x, y):        """        x: (B, dim)        y: (B, 1)        """        pass    def predict(self, x):        passif __name__ == '__main__':    batch_size, dim = 16, 2    x = np.random.uniform(-5, 5, size=(batch_size, dim))    w = np.random.uniform(-5, 5, size=(dim, 1))    y = x @ w + 10    lr = LinearReg(dim=1)    lr.fit(x, y)    y_hat = lr.predict(x)    assert np.allclose(y, y_hat)

(6)

手撕 MHA、MQA 和 GQA:

import torchimport torch.nn as nnclass MultiHeadAttention(nn.Module):    def __init__(self, d_model, num_heads):        r"""        d_model: Total dimension of the model.        num_heads:        - Number of parallel attention heads.         - Note that embed_dim will be split across num_heads (i.e. each head will have dimension embed_dim // num_heads).        """        super().__init__()    def forward(self, query, key, value):        """        query: (B, L, D)        key: (B, L, D)        value: (B, L, D)        out: (B, L, D)        """        return outif __name__ == '__main__':    B, L, D = 16, 32, 64    H = 4    mha = MultiHeadAttention(D, H)    q = torch.rand(B, L, D)    k = torch.rand(B, L, D)    v = torch.rand(B, L, D)    y = mha(q, k, v)

(7)

手撕 RMSNorm 和 LayerNorm

class RMSNorm(nn.Module):    def __init__(self, dim, eps=1e-6) -> None:        super().__init__()    def _norm(self, x):        return pass    def forward(self, x):        return passclass LayerNorm(nn.Module):    def __init__(self, dim, eps=1e-6) -> None:        super().__init__()    def _norm(self, x):    def forward(self, x):

(8)

手撕 MoE 的实现,并聊一聊为什么 FNN 里面一般的 hidden dim 都是输入 dim 的好几倍?

(9)

手撕 GRPO 和 GSPO 的简单版本,不考虑 mask_mean 之类的东西:

from email import policyimport torchfrom torch.nn.modules import lossdef gspo_loss(    advantage,  # (B, 1)    policy_logprob, # (B, L)    old_policy_logprob, # (B, L)    clip_low,     clip_high    ):    return def grpo_loss(    advantage,  # (B, 1)    policy_logprob, # (B, L)    old_policy_logprob, # (B, L)    clip_low,     clip_high    ):    returnif __name__ == '__main__':    B, L, D = 16, 32, 64    advantage = torch.rand(B, 1)    policy_logprob = torch.rand(B, L)    old_policy_logprob = torch.rand(B, L)    loss = grpo_loss(advantage, policy_logprob, old_policy_logprob, 0.2, 0.28)    print(loss)    loss = gspo_loss(advantage, policy_logprob, old_policy_logprob, 0.2, 0.28)    print(loss)

(10)

手撕一下 GAE 的代码实现:

import torchdef compute_advantage(    rewards,    values,    gamma,    lamda,):    advantages = []    batch_size, response_length = rewards.shape    for t in reversed(range(response_length)):          # TODO:    advantages = advantages[::-1]    return torch.stack(advantages, dim=1)  # 建议返回 tensor 而不是 list

02

答案

(4)

使用 naive 的 DP 是 O(N²),比较简单好写,这里主要记录 O(N log N)的做法:

class Solution:    def lengthOfLIS(self, nums: List[int]) -> int:        tail = []        tail.append(-1e9)        for i in range(0, len(nums)):            left, right = 0, len(tail) - 1            if nums[i] > tail[right]:                tail.append(nums[i])                continue            while left <= right:                if nums[i] > tail[left]:                    left += 1                else:                     right -= 1            tail[left] = nums[i]        return len(tail) - 1

(5)

使用闭式解:

import numpy as npclass LinearReg:    def __init__(self, dim) -> None:        self.W = None    def fit(self, x, y):        """        x: (B, dim)        y: (B, 1)        """        x = np.concat((x,[[1]] * len(x)), axis=-1)        self.W = np.linalg.inv(x.T @ x) @ x.T @ y    def predict(self, x):        return np.concat((x,[[1]] * len(x)), axis=-1) @ self.Wif __name__ == '__main__':    batch_size, dim = 16, 2    x = np.random.uniform(-5, 5, size=(batch_size, dim))    w = np.random.uniform(-5, 5, size=(dim, 1))    y = x @ w + 10    lr = LinearReg(dim=1)    lr.fit(x, y)    y_hat = lr.predict(x)    assert np.allclose(y, y_hat)

使用梯度下降:

import numpy as npclass LinearRegGD:    def __init__(self, dim, lr=0.01, epochs=1000, tol=1e-6) -> None:        self.dim = dim        self.lr = lr        self.epochs = epochs        self.tol = tol        self.W = None    def fit(self, x, y):        """        x: (B, dim)        y: (B, 1)        """        B = x.shape[0]        # 添加偏置列        x_bias = np.hstack([x, np.ones((B, 1))])        # 初始化权重(包含偏置)        self.W = np.random.randn(self.dim + 1, 1) * 0.01        for i in range(self.epochs):            # 预测            y_pred = x_bias @ self.W            # 梯度:X^T (XW - y) / B  (MSE的梯度)            grad = x_bias.T @ (y_pred - y) / B            # 更新            new_W = self.W - self.lr * grad            # 收敛检查            if np.linalg.norm(new_W - self.W) < self.tol:                print(f"Converged at epoch {i}")                break            self.W = new_W    def predict(self, x):        B = x.shape[0]        x_bias = np.hstack([x, np.ones((B, 1))])        return x_bias @ self.Wif __name__ == '__main__':    np.random.seed(42)    batch_size, dim = 16, 2    x = np.random.uniform(-5, 5, size=(batch_size, dim))    w_true = np.random.uniform(-5, 5, size=(dim, 1))    y = x @ w_true + 10    lr = LinearRegGD(dim=dim, lr=0.1, epochs=5000)    lr.fit(x, y)    y_hat = lr.predict(x)    print(f"True weights:\n{w_true}\nTrue bias: 10")    print(f"Learned weights:\n{lr.W[:-1]}\nLearned bias: {lr.W[-1, 0]:.4f}")    # 梯度下降不会完全等于闭式解,但应该很接近    assert np.allclose(y, y_hat, atol=1e-3), f"Max error: {np.max(np.abs(y - y_hat))}"    print("✅ 梯度下降测试通过")

梯度下降法的分析【来自 kimi】:

我来详细推导这个梯度公式。

线性回归使用均方误差(MSE):

其中:

展开:

(因为标量的转置等于自身,所以两项相等)

第二步:求 de

(x,y 是常数)

第三步:合并

因此:

第四步:加上系数 1/2B

对应代码:

grad = x_bias.T @ (y_pred - y) / B

为什么除以 B?

梯度下降更新规则:

除以 B (样本数)是为了:

  • 梯度归一化:使梯度大小不随 batch size 变化,学习率 α 更稳定

  • 等价于平均损失:1/B 对应 MSE 的平均形式

如果不除以 B,batch size 变大时梯度也会变大,需要相应调小学习率。

对比:矩阵求导法则(更直接)

直接用迹运算(trace)技巧:

结果一致。

(6)

import torchimport torch.nn as nnclass MultiHeadAttention(nn.Module):    def __init__(self, d_model, num_heads):        r"""        d_model: Total dimension of the model.        num_heads:        - Number of parallel attention heads.         - Note that embed_dim will be split across num_heads (i.e. each head will have dimension embed_dim // num_heads).        """        super().__init__()        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)        self.num_heads = num_heads    def forward(self, query, key, value):        """        query: (B, L, D)        key: (B, L, D)        value: (B, L, D)        """        B, L, D = query.shape        query = self.W_q(query).view(B, L, self.num_heads, -1).transpose(1, 2)        key = self.W_k(key).view(B, L, self.num_heads, -1).transpose(1, 2).transpose(2, 3)        value = self.W_v(value).view(B, L, self.num_heads, -1).transpose(1, 2)        d = query.size(-1)        dot_product = torch.softmax(query @ key / torch.sqrt(torch.tensor(d)), dim=-1)        out = (dot_product @ value).transpose(1, 2).reshape(B, L, -1)        out = self.W_o(out)        return outclass MultiQueryAttention(nn.Module):    """    不同的head共享一份key和value,但是query还是独立的    """    def __init__(self, d_model, num_heads):        super().__init__()        assert d_model % num_heads == 0        self.num_heads = num_heads        self.head_dim = d_model // num_heads        self.W_q = nn.Linear(d_model, d_model)        self.W_k = nn.Linear(d_model, self.head_dim)        self.W_v = nn.Linear(d_model, self.head_dim)        self.W_o = nn.Linear(d_model, d_model)    def forward(self, query, key, value):        B, L, D = query.shape        query = self.W_q(query).view(B, L, self.num_heads, -1).transpose(1, 2) # (B, num_heads, L, head_dim)        key = self.W_k(key).view(B, L, 1, -1).transpose(1, 2).transpose(2, 3)        value = self.W_v(value).view(B, L, 1, -1).transpose(1, 2)        d = query.size(-1)        dot_product = torch.softmax(query @ key / torch.sqrt(torch.tensor(d)), dim=-1)        out = (dot_product @ value).transpose(1, 2).reshape(B, L, -1)        out = self.W_o(out)        return outclass GroupedQueryAttention(nn.Module):    """    GQA: 同一组内的多个 head 共享同一份 K 和 V,每个 head 仍有独立的 Q。    num_groups 个 group,每组 group_size = num_heads // num_groups 个 head。    """    def __init__(self, d_model, num_heads, num_groups):        super().__init__()        assert d_model % num_heads == 0        self.num_heads = num_heads        assert num_heads % num_groups == 0        self.num_groups = num_groups        self.group_size = num_heads // num_groups        self.head_dim = d_model // num_heads        self.W_q = nn.Linear(d_model, d_model)        self.W_k = nn.Linear(d_model, self.num_groups * self.head_dim)        self.W_v = nn.Linear(d_model, self.num_groups * self.head_dim)        self.W_o = nn.Linear(d_model, d_model)    def forward(self, query, key, value):        B, L, D = query.shape        H = self.num_heads        G = self.num_groups        d = self.head_dim        group_size = self.group_size        # ---- Q ----        # (B, L, D) → (B, H, L, d)        q = self.W_q(query).view(B, L, H, d).transpose(1, 2)        # ---- K ----        # (B, L, G*d) → (B, G, L, d)        k = self.W_k(key).view(B, L, G, d).transpose(1, 2)        # ---- V ----        # (B, L, G*d) → (B, G, L, d)        v = self.W_v(value).view(B, L, G, d).transpose(1, 2)        # ---- broadcast KV to heads ----        # (B,G,L,d) → (B,G,group_size,L,d)        k = k.unsqueeze(2).expand(B, G, group_size, L, d)        v = v.unsqueeze(2).expand(B, G, group_size, L, d)        # (B,G,group_size,L,d) → (B,H,L,d)        k = k.reshape(B, H, L, d)        v = v.reshape(B, H, L, d)        # ---- attention ----        scale = 1.0 / math.sqrt(d)        attn = torch.matmul(q, k.transpose(-2, -1)) * scale        attn = F.softmax(attn, dim=-1)        out = torch.matmul(attn, v)        # ---- merge heads ----        out = out.transpose(1, 2).reshape(B, L, D)        return self.W_o(out)        return outif __name__ == '__main__':    B, L, D = 16, 32, 64    H = 4    mha = MultiHeadAttention(D, H)    q = torch.rand(B, L, D)    k = torch.rand(B, L, D)    v = torch.rand(B, L, D)    y = mha(q, k, v)

(7)

class LayerNorm(nn.Module):    def __init__(self, dim, eps=1e-6) -> None:        super().__init__()        self.w = nn.Parameter(torch.ones(dim))        self.b = nn.Parameter(torch.zeros(dim))        self.eps = eps    def _norm(self, x):        return (x - x.mean(dim=-1, keepdim=True)) / torch.sqrt(torch.var(x, dim=-1, keepdim=True, unbiased=False) + self.eps)    def forward(self, x):        return self.w * self._norm(x) + self.bclass RMSNorm(nn.Module):    def __init__(self, dim, eps=1e-6) -> None:        super().__init__()        self.w = nn.Parameter(torch.ones(dim))        self.eps = eps    def _norm(self, x):        return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)    def forward(self, x):        return self.w * self._norm(x)

(8)

import torchimport torch.nn as nnimport mathimport torch.nn.functional as Fclass Expert(nn.Module):    def __init__(self, n_embd, dropout=0.1):        super().__init__()        self.net = nn.Sequential(            nn.Linear(n_embd, 4 * n_embd),            nn.ReLU(),            nn.Linear(4 * n_embd, n_embd),            nn.Dropout(dropout),        )    def forward(self, x):        return self.net(x)class TopkRouter(nn.Module):    def __init__(self, n_embed, num_experts, top_k):        super(TopkRouter, self).__init__()        self.top_k = top_k        self.linear =nn.Linear(n_embed, num_experts)    def forward(self, mh_output):        """        mh_output: (B, L, D)        return: (B, L, num_experts)        """        logits = self.linear(mh_output)        # 获取前K大的值和索引,沿列。        top_k_logits, indices = logits.topk(self.top_k, dim=-1)        # 创建一个形状和logits相同全'-inf'矩阵        zeros = torch.full_like(logits, float('-inf'))        # 按照索引和值填充上述zeros矩阵        sparse_logits = zeros.scatter(-1, indices, top_k_logits)        # 对其进行softmax,未被填充的位置会为0        router_output = F.softmax(sparse_logits, dim=-1)        return router_output, indicesclass SparseMoE(nn.Module):    def __init__(self, n_embd, n_experts, top_k, dropout=0.1):        super().__init__()        self.top_k = top_k        self.n_experts = n_experts        self.experts = nn.ModuleList([Expert(n_embd, dropout) for _ in range(n_experts)])        self.router = TopkRouter(n_embd, n_experts, top_k)    def forward(self, x):        """        x: (B, L, D)        return: (B, L, D)        """        B, L, D = x.shape        router_output, indices = self.router(x) # (B, L, num_experts), (B, L, top_k)        x = torch.flatten(x, end_dim=1)        router_output = torch.flatten(router_output, end_dim=1) # (F, num_experts)        router_mask = router_output > 0 # (B, L, num_experts) -> (F, num_experts)        output = torch.zeros_like(x)        for i in range(self.n_experts):            current_expert_router = router_mask[:, i] # 当前expert需要处理的那些token的索引            current_expert_input = x[current_expert_router]            current_expert_output = self.experts[i](current_expert_input)            output[current_expert_router] += router_output[current_expert_router, i].unsqueeze(-1) * current_expert_output        return outputif __name__ == '__main__':    B, L, D = 8, 32, 64    num_experts, top_k = 4, 2    x = torch.rand(B, L, D)    smoe = SparseMoE(n_embd=D, n_experts=num_experts, top_k=top_k)    y = smoe.forward(x)    print(y.shape)

(9)

from email import policyimport torchfrom torch.nn.modules import lossdef gspo_loss(    advantage,  # (B, 1)    policy_logprob, # (B, L)    old_policy_logprob, # (B, L)    clip_low,     clip_high    ):    # ratio = torch.exp(1/ L * \sum log ratio)    negative_approx_kl = policy_logprob - old_policy_logprob    logratio = policy_logprob - policy_logprob.detach() + negative_approx_kl.detach()    ratio = torch.exp(logratio.mean(dim=-1))    loss2 = torch.clamp(ratio, 1 - clip_low, 1 + clip_high) * advantage    loss1 = ratio * advantage     loss = torch.min(loss1, loss2)    return -loss.mean()def grpo_loss(    advantage,  # (B, 1)    policy_logprob, # (B, L)    old_policy_logprob, # (B, L)    clip_low,     clip_high    ):    ratio = torch.exp(policy_logprob - old_policy_logprob)    # loss = - min(ratio * adv, clamp(ratio, 1-clip_low, 1+clip_high) * adv)    loss2 = torch.clamp(ratio, 1 - clip_low, 1 + clip_high) * advantage    loss1 = ratio * advantage     loss = torch.min(loss1, loss2)    return -loss.mean()if __name__ == '__main__':    B, L, D = 16, 32, 64    advantage = torch.rand(B, 1)    policy_logprob = torch.rand(B, L)    old_policy_logprob = torch.rand(B, L)    loss = grpo_loss(advantage, policy_logprob, old_policy_logprob, 0.2, 0.28)    print(loss)    loss = gspo_loss(advantage, policy_logprob, old_policy_logprob, 0.2, 0.28)    print(loss)

(10)

import torchdef compute_advantage(    rewards,    values,    gamma,    lamda,):    last_gae = 0    advantages = []    batch_size, response_length = rewards.shape    for t in reversed(range(response_length)):        next_value = torch.zeros(batch_size, 1) if t == response_length - 1 else values[:, t + 1]        # 修正:r + gamma * V(s_{t+1}) - V(s_t)        delta = rewards[:, t] + gamma * next_value - values[:, t]        gae = delta + gamma * lamda * last_gae        advantages.append(gae)        last_gae = gae  # 更新 last_gae 供下一次迭代使用    advantages = advantages[::-1]    return torch.stack(advantages, dim=1)  # 建议返回 tensor 而不是 list

作者:Pier,已获作者授权发布

来源:https://zhuanlan.zhihu.com/p/2012305348481015991

END
加入学习

✅我是丁师兄,专注于智能驾驶大模型,持续分享LLM面试干货。

✅大模型1v1辅导,已帮助多名同学成功上岸

微信:dsxaigc

相关学习资料