大厂真题 / 华为
华为AI岗 2026-09-16
内容边界:材料声明本场有 20 道选择题,但正文只展开了第 1 至第 10 题;本文收录这 10 道可核对选择题和 2 道完整编程题,不补写缺失的第 11 至第 20 题。
本场考试概述
考试时间:2026-09-16
考试岗位:AI岗
难度评级:中等
考点分析:
- 选择题:可恢复 10 道,涉及训练工程、降维、多模态、线性代数、概率论、强化学习与 Transformer。
- 第一题 实现 Transformer 编码器层:多头自注意力、Post-LN、前馈网络(难度中等)。
- 第二题 核心路由器管理域规划:二分 K-Means 与规则模拟(难度中等)。
建议策略:
- 训练工程题先抓住访存、显存状态和参数更新对象,避免只凭术语作答。
- Transformer 题严格按每头维度缩放,并在最后一维使用总体方差做层归一化。
- 二分 K-Means 题把初始化、收敛、选簇和平局规则逐条落到代码中,生成顺序不能丢。
选择题(可恢复 10 道)
单选题
1、实现 LayerNorm 融合 Kernel 时,原方案需要两次遍历同一输入,第一次计算均值,第二次计算方差。以下优化策略正确的是:
A. 使用 Welford 在线算法,单次遍历同时计算均值和方差,避免再次从 HBM 读取数据
B. 将规约改成近似计算
C. 跳过方差计算,改用固定值
D. 用两个独立 Kernel 分别计算均值和方差,中间结果写回 HBM
答案:A
解析:Welford 递推可在一次扫描中更新均值与离差平方和,减少一次全量访存,同时保留所需统计量。B、C 会改变算子语义;D 仍有额外 Kernel 启动和 HBM 往返,违背融合目的。
2、关于 PCA 与 LDA 在特征提取逻辑上的本质区别,正确的是:
A. LDA 寻找类内方差尽量小、类间方差尽量大的投影空间
B. PCA 得到的是原始特征的非线性组合
C. PCA 是通过类别标签最大化类间差异的有监督方法
D. LDA 是寻找最大方差方向的无监督方法
答案:A
解析:LDA 使用标签并优化类间散度与类内散度之比;PCA 不使用标签,寻找数据方差最大的正交方向,得到的是原始特征的线性组合。
3、将一维连续音频波形转换为类似图像的二维特征图,最常用的表示是:
A. 傅里叶逆变换波形
B. 梅尔频谱图
C. 音频采样点序列
D. MIDI 信号
答案:B
解析:短时傅里叶变换先得到时间与频率的二维能量分布,再按梅尔尺度压缩频率轴,形成梅尔频谱图。采样点序列仍是一维;MIDI 是符号事件;傅里叶逆变换的方向相反。
4、Cholesky 分解把矩阵写成 $A=LL^T$,其中 $L$ 为下三角矩阵。它适用于:
A. 上三角矩阵
B. 对称正定矩阵
C. 三对角矩阵
D. 任意方阵
答案:B
解析:$LL^T$ 必然对称,并且对任意非零向量 $x$,有 $x^TLL^Tx=\lVert L^Tx\rVert^2>0$。对称正定矩阵也存在对角元为正的 Cholesky 分解。
5、随机变量 $X$ 服从参数为 $p$ 的几何分布,$P(X=k)=p(1-p)^{k-1}$,$k=1,2,\ldots$。其期望为:
A. $1/(1-p)$
B. $1/p$
C. $(1-p)/p$
D. $p/(1-p)$
答案:B
解析:这里 $X$ 统计首次成功所需的试验次数,因此 $E(X)=1/p$。$(1-p)/p$ 对应“首次成功前失败次数”的另一种计数定义。
6、若方阵 $A$ 的行列式满足 $\det(A)=0$,说明该矩阵:
A. 可逆
B. 正定
C. 奇异、不可逆
D. 对称
答案:C
解析:方阵可逆当且仅当行列式非零。行列式为零说明矩阵不满秩,因此是奇异矩阵;该条件不能单独判断对称性。
7、逻辑回归主要用于解决:
A. 降维问题
B. 连续值回归问题
C. 二分类问题
D. 聚类问题
答案:C
解析:逻辑回归把线性输出经 Sigmoid 映射为类别概率,再按阈值分类,是经典的有监督二分类模型。
8、在 RLHF 流程中,PPO 的作用是:
A. 对模型做预训练
B. 微调语言模型以最大化奖励
C. 生成人类反馈标注
D. 训练奖励模型
答案:B
解析:PPO 把语言模型视作策略,依据奖励信号更新策略,同时用裁剪比率和 KL 约束限制更新幅度。奖励模型训练和人工偏好标注发生在这一阶段之前。
9、DeepSpeed ZeRO-3 相比 ZeRO-1,额外分片了哪些显存状态:
A. 激活值
B. 梯度和模型参数
C. 优化器状态
D. 只有梯度
答案:B
解析:ZeRO-1 分片优化器状态,ZeRO-2 再分片梯度,ZeRO-3 进一步分片模型参数。因此从 ZeRO-1 到 ZeRO-3 新增的是梯度与参数分片。
10、Transformer 解码器为了防止当前位置看到未来信息,通常使用:
A. 层归一化
B. 残差连接
C. 位置编码
D. 因果掩码
答案:D
解析:因果掩码把注意力矩阵中指向未来位置的分数屏蔽掉,使第 $i$ 个位置只能关注自身及之前的位置。其他三个组件都不会限制信息流向。
第 1 题:实现 Transformer 编码器层
题目描述
实现一个标准的 Transformer Encoder Layer。该层依次执行:
- 多头自注意力;
- 与输入做残差相加,再做层归一化;
- 逐位置前馈网络;
- 再做一次残差相加和层归一化。
本题固定 $d_{model}=4$、注意力头数 $h=2$、每头维度 $d_k=2$,FFN 隐藏维度为 4。$W_Q,W_K,W_V,W_O,W_1,W_2$ 都是 $4\times4$ 单位矩阵,偏置均为 0。两次层归一化的 $\gamma=1$、$\beta=0$、$\varepsilon=10^{-5}$,在最后一维上按总体方差计算。
缩放点积注意力为:
\[\operatorname{Attention}(Q,K,V)=\operatorname{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]输入描述
一行 8 个实数,表示形状为 $(1,2,4)$ 的输入张量按顺序展开后的结果。每个输入在 $[-100,100]$ 内,保留两位小数。
输出描述
一行 8 个实数,表示输出张量展开后的结果,每个数保留两位小数。
样例 1
输入
11.00 12.00 3.00 1.00 15.00 16.00 10.00 11.00
输出
0.81 1.17 -0.95 -1.04 0.72 1.24 -1.11 -0.85
样例 2
输入
1.00 2.00 3.00 4.00 5.00 6.00 7.00 8.00
输出
-1.18 -0.59 0.30 1.47 -1.18 -0.59 0.29 1.47
思路分析
第一步:拆分注意力头。 两个 token 都有 4 维,把前两维交给第一个头、后两维交给第二个头。权重全是单位矩阵,所以 $Q=K=V=X$。
第二步:逐头计算注意力。 对每个查询 token,计算它和两个键的点积,除以 $\sqrt{2}$ 后做数值稳定的 softmax,再对两个值向量加权求和。缩放因子使用每头维度 $d_k$,不是总维度 $d_{model}$。
第三步:完成两次 Post-LN。 拼接两个头的输出,与原输入相加并归一化。FFN 的两个权重都是单位矩阵,所以它退化为逐元素 ReLU;再次残差相加并归一化即可。
正确性说明
代码按维度拆出两个互不混合的注意力头,并直接实现缩放点积注意力定义,因此每个头的输出正确。拼接恢复原来的特征顺序后,依题面依次执行残差、层归一化、单位权重 FFN 和第二次残差归一化,所以最终结果与给定编码器层的计算顺序完全一致。
题解代码
import sys
from math import exp, sqrt
D_MODEL = 4
D_K = 2
EPS = 1e-5
def layer_norm(values):
mean = sum(values) / D_MODEL
variance = sum((value - mean) ** 2 for value in values) / D_MODEL
scale = sqrt(variance + EPS)
return [(value - mean) / scale for value in values]
def solve():
values = list(map(float, sys.stdin.buffer.read().split()))
x = [values[:D_MODEL], values[D_MODEL:]]
head_outputs = []
for start in (0, D_K):
head_values = [row[start:start + D_K] for row in x]
output = []
for query in head_values:
scores = [
sum(a * b for a, b in zip(query, key)) / sqrt(D_K)
for key in head_values
]
maximum = max(scores)
weights = [exp(score - maximum) for score in scores]
total = sum(weights)
output.append([
sum(weights[j] * head_values[j][d] for j in range(2)) / total
for d in range(D_K)
])
head_outputs.append(output)
attention = [
head_outputs[0][token] + head_outputs[1][token]
for token in range(2)
]
hidden = [
layer_norm([x[token][d] + attention[token][d] for d in range(D_MODEL)])
for token in range(2)
]
output = [
layer_norm([value + max(0.0, value) for value in hidden[token]])
for token in range(2)
]
result = []
for value in (item for row in output for item in row):
text = f"{value:.2f}"
result.append("0.00" if text == "-0.00" else text)
print(" ".join(result))
solve()
复杂度分析
时间复杂度:一般序列长度为 $T$、模型维度为 $d$ 时,注意力为 $O(T^2d)$,线性层为 $O(Td^2)$;本题 $T=2,d=4$,实际为常数量级。
空间复杂度:一般为 $O(T^2h+Td)$;本题参数固定,实际为 $O(1)$。
易错点
- softmax 前应减去该行最大值,避免指数溢出。
- 方差除以 4,使用总体方差;不能改成样本方差。
- 极小负数格式化后可能成为
-0.00,题面要求输出0.00。
第 2 题:核心路由器管理域规划
题目描述
给定 $L$ 个二维点,使用 Bi-K-Means 把它们从一个初始簇逐步分裂为 $N$ 个簇。簇的误差平方和为:
\[\operatorname{SSE}(C)=\sum_{p\in C}\lVert p-\mu_C\rVert^2\]每次分裂一个簇时执行 $K=2$ 的 K-Means,并遵循以下规则:
- 两个初始质心分别取当前簇中 $x$ 坐标最小和最大的点;所有点的 $x$ 坐标互不相同。
- 每轮先分配点、再更新质心。材料未单列等距归组规则;为复现其参考实现,本文约定等距时归入第二个子簇。
- 所有点归属不再变化,或两个新质心的最大移动距离小于 $10^{-6}$ 时停止。
- 待分裂簇按 SSE 下降量从大到小选择;差值不超过 $10^{-6}$ 视为相同,再优先点数更多的簇,仍相同时选择更早生成的簇。
- 删除父簇后,两个子簇依次追加到簇列表末尾。
SSE 下降量为父簇 SSE 减去两个子簇 SSE 之和。
输入描述
第一行是目标簇数 $N$,$1\le N\le20$。
第二行是点数 $L$,$1\le L\le100$。
接下来 $L$ 行每行两个整数 $x,y$,满足 $0\le x,y\le1000$,且不同点的 $x$ 坐标互不相同。
输出描述
先输出初始簇的点数。每完成一次分裂,再输出当前所有簇的点数降序序列,共输出到簇数达到 $N$。
样例 1
输入
2
3
1 1
2 2
6 6
输出
3
2 1
样例 2
输入
1
2
2 3
5 5
输出
2
思路分析
第一步:实现一次确定性的二分。 初始质心由最小和最大 $x$ 唯一决定。每轮按平方距离分组,再把质心更新为组内均值,直到满足任一停止条件。固定等距归组规则后,分裂结果可以复现。
第二步:评估全部候选簇。 单点簇不能分裂。对每个其他簇试做一次二分,计算 SSE 下降量。比较时依次使用下降量、簇大小和生成时间三个关键字。
第三步:维护生成顺序。 簇列表本身按生成时间排列。平局时只在候选严格更优或同下降量但点数更多时替换当前答案,就会自然保留更早生成的簇。父簇删除后把两个孩子追加到末尾,顺序继续成立。
正确性说明
split_two 严格执行题面的初始化、分配、更新和停止规则,因此返回指定 K-Means 过程的两个子簇。choose_cluster 枚举全部可分裂簇,并按三层优先级保留唯一胜者,所以每轮选择正确。循环从一个簇开始,每轮删除一个父簇并加入两个子簇,簇数恰好增加一;因此输出覆盖从 1 个簇到 $N$ 个簇的全部状态。
题解代码
import sys
from math import hypot
EPS = 1e-6
def sse(points):
mean_x = sum(x for x, _ in points) / len(points)
mean_y = sum(y for _, y in points) / len(points)
return sum((x - mean_x) ** 2 + (y - mean_y) ** 2 for x, y in points)
def split_two(points):
left = min(points, key=lambda point: point[0])
right = max(points, key=lambda point: point[0])
centers = [[float(left[0]), float(left[1])],
[float(right[0]), float(right[1])]]
labels = None
while True:
new_labels = []
for x, y in points:
distance_0 = (x - centers[0][0]) ** 2 + (y - centers[0][1]) ** 2
distance_1 = (x - centers[1][0]) ** 2 + (y - centers[1][1]) ** 2
new_labels.append(0 if distance_0 < distance_1 else 1)
if new_labels == labels:
break
labels = new_labels
new_centers = []
maximum_move = 0.0
for label in range(2):
group = [points[i] for i in range(len(points)) if labels[i] == label]
center_x = sum(x for x, _ in group) / len(group)
center_y = sum(y for _, y in group) / len(group)
maximum_move = max(
maximum_move,
hypot(center_x - centers[label][0], center_y - centers[label][1]),
)
new_centers.append([center_x, center_y])
centers = new_centers
if maximum_move < EPS:
break
first = [points[i] for i in range(len(points)) if labels[i] == 0]
second = [points[i] for i in range(len(points)) if labels[i] == 1]
return first, second
def choose_cluster(clusters):
best_index = -1
best_gradient = 0.0
best_children = None
for index, cluster in enumerate(clusters):
if len(cluster) < 2:
continue
first, second = split_two(cluster)
gradient = sse(cluster) - sse(first) - sse(second)
better = gradient > best_gradient + EPS
tied = abs(gradient - best_gradient) <= EPS
larger = best_index == -1 or len(cluster) > len(clusters[best_index])
if best_index == -1 or better or (tied and larger):
best_index = index
best_gradient = gradient
best_children = (first, second)
return best_index, best_children
def solve():
data = list(map(int, sys.stdin.buffer.read().split()))
target_count = data[0]
point_count = data[1]
points = [tuple(data[i:i + 2]) for i in range(2, 2 + 2 * point_count, 2)]
clusters = [points]
print(point_count)
for _ in range(target_count - 1):
index, children = choose_cluster(clusters)
clusters.pop(index)
clusters.extend(children)
print(" ".join(map(str, sorted((len(cluster) for cluster in clusters), reverse=True))))
solve()
复杂度分析
时间复杂度:$O(NLI)$,其中 $I$ 是一次二分 K-Means 的迭代轮数。每一轮分裂评估的所有簇总点数为 $L$。
空间复杂度:$O(L)$,不计输入与输出之外,每个点只属于当前簇列表中的一个簇。
易错点
- 比较下降量时使用 $10^{-6}$ 容差,不能直接用浮点数相等。
- 点到两个质心等距时的归组会影响后续质心,必须固定规则。
- 子簇要追加到列表末尾,不能原地替换父簇后打乱生成时间。
小结
- 可见选择题重点覆盖训练工程、数学基础和 Transformer 概念;缺失的 10 题不作推测。
- Transformer 题的核心是按定义还原张量计算,并处理 softmax、LayerNorm 和输出格式的数值细节。
- 二分 K-Means 题的算法并不复杂,真正的难点是完整实现所有确定性规则。