大厂真题 / 华为

华为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。该层依次执行:

  1. 多头自注意力;
  2. 与输入做残差相加,再做层归一化;
  3. 逐位置前馈网络;
  4. 再做一次残差相加和层归一化。

本题固定 $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,并遵循以下规则:

  1. 两个初始质心分别取当前簇中 $x$ 坐标最小和最大的点;所有点的 $x$ 坐标互不相同。
  2. 每轮先分配点、再更新质心。材料未单列等距归组规则;为复现其参考实现,本文约定等距时归入第二个子簇。
  3. 所有点归属不再变化,或两个新质心的最大移动距离小于 $10^{-6}$ 时停止。
  4. 待分裂簇按 SSE 下降量从大到小选择;差值不超过 $10^{-6}$ 视为相同,再优先点数更多的簇,仍相同时选择更早生成的簇。
  5. 删除父簇后,两个子簇依次追加到簇列表末尾。

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 题的算法并不复杂,真正的难点是完整实现所有确定性规则。