大厂真题 / 华为
华为 8.19 笔试真题 - AI 岗
本场考试概述
考试时间:2026 年 8 月 19 日
考试岗位:AI 岗
难度评级:中等
考点分析:
- 选择题(20 道):高维几何、概率统计、机器学习、RNN、混合精度、Transformer、FlashAttention、PagedAttention、MoE 与 RoPE。
- 流水线并行最小瓶颈:二分答案 + 贪心可行性检查。
- FlashAttention 分块合并:自定义结合运算 + 线段树。
两道编程题都以大模型系统为背景,但算法内核分别是“连续数组最小化最大段和”和“支持单点更新的区间幺半群查询”。
选择题(20 道)
部分题面中的具体数值在整理材料中缺失。以下仅保留能够可靠确认的题意、答案与知识点;不完整的数值题不补造参数和选项。
一、单选题
1. 高维随机向量
在极高维特征空间中,独立均匀采样的两个单位向量,其余弦相似度分布最可能呈现什么特点?
答案:高度集中在 0 附近,随机向量几乎正交。
解析:在各向同性假设下,内积期望为 0、方差随维度按 $1/d$ 缩小,维度越高,余弦相似度越集中于 0。
2. FlashAttention 的 Tiling
FlashAttention 中分块策略的主要目的是什么?
答案:减少 HBM 访问,将中间计算尽量留在片上 SRAM 中完成。
解析:分块并不会增加参数量,也不是模型并行。其核心收益是避免显式写回完整注意力矩阵,降低高代价的显存读写。
3. 线性变换的表示
有限维线性空间中的任意线性变换可以通过哪种结构表示?
答案:矩阵乘法。
解析:确定线性变换在一组基上的像,即可将这些像按列组成矩阵;任意向量的变换由该矩阵乘以坐标向量得到。
4. 层次凝聚聚类的内存优化
处理大规模数据集时,怎样降低层次凝聚聚类维护全量距离关系的内存开销?
答案:采用稀疏近邻图替代稠密全量距离矩阵。
解析:全量距离矩阵需要 $O(n^2)$ 空间;只保留局部近邻关系可显著降低存储量,但属于近似或受图结构约束的方法,是否保持原聚类结果取决于具体数据和算法。
5. 音频条件如何注入视频生成
音视频联合生成模型通常怎样把音频条件注入视频扩散模型?
答案:先用音频编码器提取时序特征,再通过交叉注意力或自适应归一化等机制注入视频 UNet 的相关层。
解析:原始波形与视频帧的采样尺度不一致,通常不能直接拼接;训练阶段也必须让条件通路参与学习。
6. RNN 的长序列问题
RNN 处理长序列时最典型的训练问题是什么?
答案:梯度消失或梯度爆炸。
解析:时间反向传播包含循环权重矩阵的反复连乘,谱半径偏离 1 时,梯度会指数衰减或放大。
7. 编码器—解码器与仅解码器架构
原始 Transformer 的编码器—解码器架构与 GPT 类仅解码器架构相比,哪项区别正确?
答案:标准仅解码器架构没有独立编码器,因此不包含“解码器查询编码器输出”的交叉注意力子层。
解析:仅解码器通常采用因果自注意力;原始 Transformer 解码器既有带掩码的自注意力,也有面向编码器输出的交叉注意力。
8. 条件概率
该题考查互斥事件与条件概率。可靠的求解框架是:先用互斥性拆分交事件,再按
\[P(A\mid B)=\frac{P(A\cap B)}{P(B)}\]计算。整理材料缺失事件名和数值,故不补写具体选项与数值答案。
9. PagedAttention 与物理碎片
固定页大小的 PagedAttention 能否把两段互不相邻、且各自不足一页的物理碎片拼成一页?
答案:不能。
解析:分页允许逻辑页映射到不连续的物理页,但一个物理页本身仍需有足够大的连续空间。大于等于一页的碎片可以切出整页,余量继续成为碎片。
10. 正态分布四阶中心矩
若 $X\sim\mathcal N(\mu,\sigma^2)$,则四阶中心矩是多少?
答案:
\[E[(X-\mu)^4]=3\sigma^4.\]11. 精确率
二分类中的精确率(Precision)如何定义?
答案:预测为正的样本中,实际为正的比例。
\[\text{Precision}=\frac{TP}{TP+FP}.\]12. MoE 专家并行
混合专家模型中,专家并行的核心思想是什么?
答案:把不同专家分布到不同设备,token 经路由后送到持有目标专家的设备计算。
解析:它与按注意力头或张量维度切分的张量并行不是同一概念,常需要 All-to-All 通信完成 token 调度。
13. 混合精度中的数值下溢
反向传播累积梯度时,哪种情况最容易导致下溢?
答案:将很小的梯度保存在低精度格式中并进行累加。
解析:过小的数值可能舍入为 0;混合精度训练常用 loss scaling 缓解。权重过大或学习率过大更容易导致上溢或发散。
14. GPT 预训练范式
GPT 系列的典型预训练目标是什么?
答案:在因果掩码下自回归预测下一个 token。
15. 动量法
优化器中引入动量的主要作用是什么?
答案:加速沿稳定方向的收敛,并抑制狭窄山谷中梯度来回震荡。
二、多选题
16. 正则化线性回归
以下结论正确:
- Lasso 使用 L1 惩罚,可产生稀疏系数并用于特征选择;
- 岭回归在正则参数为正时通常有唯一解;Lasso 在特征相关等情况下可能不唯一;
- 正则化强度增大通常会降低模型有效复杂度。
错误结论是“岭回归能把部分系数精确压到 0”。L2 惩罚通常只使系数连续收缩。
17. KV Cache 按页分配
与按最大长度整段预留相比,按页分配 KV Cache 的优势包括:
- 支持兼容实现中的前缀页共享;
- 显著减少最大长度预留造成的内部浪费;
- 随序列增长动态按需分配。
它不会减少模型权重本身占用的显存。
18. 推理显存优化
面对 KV Cache 显存占用与碎片问题,合理方案包括:
- 用 PagedAttention 改善 KV Cache 分配与碎片管理;
- 在精度允许时量化 KV Cache;
- 采用滑动窗口限制每层保留的历史 KV 长度。
训练中的激活重计算不能直接视作解码阶段 KV Cache 的通用替代方案;若每一步都重算历史,会显著增加延迟。
19. RoPE 的推理特性
正确描述包括:
- 在注意力内积中融入相对位置信息;
- 具有一定长度外推能力,通常还需配合缩放方法或长上下文训练;
- 不依赖固定长度的可学习绝对位置表。
RoPE 对 Q、K 做旋转,不会因此显著增大 KV Cache 的维度或数量。
20. 缓解 RNN 梯度消失
常见方法包括:
- 合理初始化循环权重,如正交初始化;
- 在适用架构中使用非饱和激活;
- 使用 LSTM 或 GRU 的门控与加法状态通路。
梯度裁剪主要针对梯度爆炸,不是解决梯度消失的手段。
第 1 题:流水线并行最小瓶颈
题目描述
一个模型有 $n$ 层,第 $i$ 层计算量为 $w_i$。需要按原顺序把这些层划分给 $k$ 个计算节点:
- 每层恰好分配一次;
- 每个节点负责一段连续且非空的层;
- 不得打乱层的顺序。
一个节点的负载是其负责层的计算量之和,系统峰值负载是所有节点负载的最大值。求峰值负载的最小可能值。
输入描述
第一行输入 n;第二行输入 n 个正整数 w_i;第三行输入节点数 k。题意默认 1 <= k <= n。
样例
输入
5
3 1 4 1 5
2
输出
8
可划分为 [3, 1, 4] 与 [1, 5],两段和为 8 和 6。
思路分析
这是“将正整数数组切成恰好 $k$ 个连续非空段,使最大段和最小”的经典问题。
对候选峰值 cap,从左向右尽量把元素装入当前段,超出 cap 时新开一段。由于所有计算量为正,这一贪心得到的是上限 cap 下所需的最少段数。
若最少段数不超过 k,则 cap 可行:在 k <= n 的前提下,可以继续拆分某些含多个元素的段,直到恰好得到 k 个非空段,且最大段和不会增加。
可行性关于 cap 单调,因此在 [max(w), sum(w)] 上二分答案。
题解代码
import sys
def solve():
input = sys.stdin.buffer.readline
n = int(input())
weights = list(map(int, input().split()))
k = int(input())
def feasible(cap):
segments = 1
current = 0
for weight in weights:
if current + weight <= cap:
current += weight
else:
segments += 1
current = weight
if segments > k:
return False
return True
left = max(weights)
right = sum(weights)
while left < right:
middle = (left + right) // 2
if feasible(middle):
right = middle
else:
left = middle + 1
print(left)
if __name__ == "__main__":
solve()
复杂度分析
设权重总和为 $W$。
- 时间复杂度:$O(n\log W)$;
- 空间复杂度:除输入数组外为 $O(1)$。
易错点
- 二分下界必须至少为最大单层计算量。
- 可行条件是贪心得到的最少段数
<= k,不是必须直接等于k。 - 上述“继续拆段”的论证依赖
k <= n且权重非负;这里由题意保证。
第 2 题:FlashAttention 分块合并
题目描述
每个序列块维护状态三元组 $(m,d,v)$:
- $m$:该块打分的最大值;
- $d$:以 $m$ 为基准重缩放后的权重和;
- $v$:以 $m$ 为基准重缩放后的加权值之和。
该状态对应的注意力结果为 $v/d$。相邻状态 $A=(m_a,d_a,v_a)$ 与 $B=(m_b,d_b,v_b)$ 合并时,令
\[m=\max(m_a,m_b),\] \[d=d_ae^{m_a-m}+d_be^{m_b-m},\] \[v=v_ae^{m_a-m}+v_be^{m_b-m}.\]该合并运算满足结合律。给定 $B$ 个按顺序排列的块,支持 $Q$ 次操作:
1 i m d v:把第i个块替换为新状态;2 l r:依次合并第l到第r个块,输出结果 $v/d$,保留 6 位小数。
样例
输入
2 3
1.0 2.0 8.0
1.0 2.0 12.0
2 1 2
2 1 1
2 2 2
输出
5.000000
4.000000
6.000000
思路分析
合并运算满足结合律,所以可以用线段树让每个节点保存对应区间的合并状态:
- 单点更新只需重算叶子到根路径,复杂度 $O(\log B)$;
- 区间查询由 $O(\log B)$ 个线段树节点组成。
尽管该公式在数学上还满足交换律,下面仍使用通用的“左累积 + 右累积”迭代查询写法,严格保持区间顺序,也便于迁移到只满足结合律而不满足交换律的算子。
为便于处理补齐叶子,定义空状态为 None;它与任意真实状态合并都返回另一方。这样不需要依赖某个有限的负数模拟负无穷,也避免题目数值范围未知时哨兵不够小的问题。
合并时统一减去两侧最大值,两个指数都不大于 0,可避免指数上溢;至少一侧指数严格为 1。只要输入状态的 d 为正,合并后的分母也为正。
题解代码
import sys
from math import exp
def merge(left, right):
if left is None:
return right
if right is None:
return left
lm, ld, lv = left
rm, rd, rv = right
maximum = max(lm, rm)
left_scale = exp(lm - maximum)
right_scale = exp(rm - maximum)
return (
maximum,
ld * left_scale + rd * right_scale,
lv * left_scale + rv * right_scale,
)
def solve():
input = sys.stdin.buffer.readline
block_count, query_count = map(int, input().split())
blocks = [tuple(map(float, input().split())) for _ in range(block_count)]
size = 1
while size < block_count:
size <<= 1
tree = [None] * (2 * size)
for index, block in enumerate(blocks):
tree[size + index] = block
for index in range(size - 1, 0, -1):
tree[index] = merge(tree[index * 2], tree[index * 2 + 1])
def update(position, state):
index = size + position
tree[index] = state
index >>= 1
while index:
tree[index] = merge(tree[index * 2], tree[index * 2 + 1])
index >>= 1
def query(left, right):
# 查询半开区间 [left, right)。
left += size
right += size
left_result = None
right_result = None
while left < right:
if left & 1:
left_result = merge(left_result, tree[left])
left += 1
if right & 1:
right -= 1
right_result = merge(tree[right], right_result)
left >>= 1
right >>= 1
return merge(left_result, right_result)
output = []
for _ in range(query_count):
operation = input().split()
if operation[0] == b"1":
position = int(operation[1]) - 1
state = tuple(map(float, operation[2:5]))
update(position, state)
else:
left = int(operation[1]) - 1
right = int(operation[2])
_, denominator, numerator = query(left, right)
output.append(f"{numerator / denominator:.6f}")
sys.stdout.write("\n".join(output))
if __name__ == "__main__":
solve()
复杂度分析
- 建树时间复杂度:$O(B)$;
- 每次更新或查询时间复杂度:$O(\log B)$;
- 空间复杂度:$O(B)$。
易错点
- 必须先减去公共最大值再取指数,避免上溢。
- 区间查询应保持块的原始顺序。
- 输出的是合并后的
v / d,不是三元组本身。 - 使用
None作为单位元比猜测一个足够小的有限哨兵更稳健。