大厂真题 / 华为

华为AI岗 2026-09-18

内容边界:该场机考声明共有 20 道选择题,材料只展开了第 1 至第 5 题。本文收录这 5 道可核对的选择题和 2 道完整编程题,不补写缺失的第 6 至第 20 题,也不补充原文未披露的题目、样例、约束与字段。

本场考试概述

考试时间:2026-09-18

考试岗位:AI岗

难度评级:简单偏中等

考点分析:

  • 选择题:可恢复 5 道。材料给出的分布是推理部署、大模型原理、RAG、评测等大模型方向约 10 道,概率论、线性代数、高等数学等数学方向约 7 道,机器学习 2 道、深度学习 1 道。
  • 第一题 稀疏局部注意力机制:候选集构造 + 排序模拟(难度中等)。
  • 第二题 流水线并行最小化最大 Stage 负载:二分答案 + 贪心(难度中等)。

建议策略:

  • 材料给出的分布显示,大模型推理在选择题里占比最高,Prefill 与 Decode 谁是计算瓶颈、算子融合省下多少显存流量、KV Cache 省的是哪部分计算,这几类都要会算会判;概率论的正态分布、假设检验与极大似然也占了不小比例。
  • 第一题照公式直译即可,重点是候选集去重、只留正分、同分时下标小的优先这三条规则;纯 Python 循环逐行算点积会超时,把每行的点积交给矩阵运算。
  • 第二题是经典的「最小化最大值」,二分负载上限、贪心数段数;段数少于 $K$ 也算可行,因为总能把某一段再拆开凑够 $K$ 段。

选择题(可恢复 5 道)

1、在标准正态分布中,随机变量落在区间 $[-1,1]$ 内的概率大约是?

A. 99%

B. 95%

C. 50%

D. 68%

答案:D

解析:正态分布的经验法则给出:落在 $[\mu-\sigma,\mu+\sigma]$ 内的概率约为 $68.27\%$,$[\mu-2\sigma,\mu+2\sigma]$ 内约为 $95.45\%$,$[\mu-3\sigma,\mu+3\sigma]$ 内约为 $99.73\%$。标准正态分布 $\mu=0$、$\sigma=1$,区间 $[-1,1]$ 正是一倍标准差,故选 D。B 对应的是两倍标准差,A 接近三倍标准差。

2、关于模型推理中前向计算与训练前向计算的区别,下列说法错误的是()

A. 推理前向计算仅需输出最终结果,训练前向计算需保留中间特征用于反向传播

B. 推理前向计算与训练前向计算的计算逻辑完全一致,仅输入数据不同

C. 推理前向计算的批量大小通常小于训练,以平衡延迟和吞吐量

D. 推理前向计算可复用 KV Cache,训练前向计算无需缓存 K/V 向量

答案:B

解析:两者的计算逻辑并不完全一致:推理时 Dropout 关闭、BatchNorm 改用训练期统计的滑动均值与方差,自回归生成还要逐 token 解码并复用缓存,所以 B 错误。A 正确,训练要保存激活值供反向传播求梯度,推理算完即可丢弃。C 正确,在线推理对延迟敏感,batch 通常比训练小。D 正确,训练时整段序列一次性并行计算,不需要跨步缓存 K/V。

3、关于旋转位置编码(RoPE,Rotary Position Embedding)的表述,正确的是?

A. RoPE 将绝对位置信息直接加到词嵌入中

B. RoPE 通过旋转矩阵对查询和键向量进行变换,使内积蕴含相对位置信息

C. RoPE 仅适用于编码器,不适用于自回归解码器

D. RoPE 无法处理超出预训练长度的序列

答案:B

解析:RoPE 把位置 $m$ 的查询和位置 $n$ 的键分别按角度 $m\theta$、$n\theta$ 旋转,二者内积只依赖 $m-n$,于是绝对位置的编码方式得到了相对位置的效果,B 正确。A 描述的是原始 Transformer 的加性位置编码,RoPE 作用在 $Q,K$ 上而不是加到嵌入里。C 错误,LLaMA、Qwen 等解码器模型都用 RoPE。D 错误,配合位置插值、NTK-aware 缩放、YaRN 等方法可以外推到更长的序列。

4、某工厂生产的产品质量指标 $X$ 服从正态分布 $N(100,16)$,即均值 $\mu=100$、方差 $\sigma^{2}=16$。现随机抽取 $25$ 个样本,测得样本均值 $\bar{X}=97.5$。在 $\alpha=0.05$ 水平下检验生产线是否正常($H_{0}:\mu=100$),已知双侧检验的临界值 $Z_{0.025}=1.96$,则结论是?

A. 接受 $H_{0}$,无显著差异,生产线正常

B. 无法判断

C. 需要更多数据

D. 拒绝 $H_{0}$,有显著差异,生产线需要调整

答案:D

解析:方差已知,用 $Z$ 检验:

\[Z=\frac{\bar{X}-\mu_{0}}{\sigma/\sqrt{n}}=\frac{97.5-100}{4/5}=-3.125\]

由于 $\lvert Z\rvert=3.125>1.96$,统计量落在拒绝域内,应拒绝 $H_{0}$,选 D。给定显著性水平和临界值之后检验结论是确定的,B、C 都不成立。

5、以下关于 Kernel 中线程组织和共享内存使用的说法,正确的是:

A. 增大 thread block 大小总能提升 Kernel 性能

B. 同一 thread block 内的线程可通过共享内存通信,不同 block 的线程无法直接通信

C. 共享内存(Shared Memory)对整个 GPU 上所有线程可见

D. 共享内存的访问速度与寄存器相同

答案:B

解析:共享内存按 block 分配,生命周期与 block 相同,只有同一 block 的线程能通过它交换数据并借助 __syncthreads() 同步;不同 block 之间只能借助全局内存,B 正确。A 错误,block 过大会让每个 SM 上能驻留的 block 数下降、寄存器和共享内存不够分,反而降低占用率。C 错误,对全体线程可见的是全局内存。D 错误,寄存器是最快的存储,共享内存比它慢,还可能有 bank conflict。


第 1 题:稀疏局部注意力机制

题目描述

给定三个整数矩阵 $Q$、$K$、$V$,大小均为 $L\times D$,再给定 $G$ 个全局位置,组成集合 $P$。

对于位置 $i$,可参与注意力计算的位置集合 $C_{i}$ 定义为

\[C_{i}= \begin{cases} \{0,1,\dots,L-1\}, & i\in P,\\ P\cup\{j\mid 0\le j<L,\ \lvert i-j\rvert\le W\}, & i\notin P. \end{cases}\]

集合中的同一位置只计算一次。

对 $j\in C_{i}$,注意力分数定义为

\[\operatorname{score}(i,j)=\sum_{x=0}^{D-1}Q[i][x]\cdot K[j][x]\]

只保留分数严格大于 $0$ 的位置,并从中选出最多 $T$ 个分数最大的候选;分数相同时,下标 $j$ 较小的位置优先。设最终选中的位置集合为 $A_{i}$,若有效候选数量不超过 $T$ 则全部保留。

输出矩阵 $O$ 定义为

\[O[i][x]=\sum_{j\in A_{i}}\operatorname{score}(i,j)\cdot V[j][x],\qquad 0\le x<D\]

若 $A_{i}$ 为空,则 $O[i][x]=0$。请输出最终的 $L\times D$ 矩阵 $O$。

输入描述

第一行包含五个整数 $L,D,G,W,T$,含义为序列长度、特征维度、全局位置数量、局部窗口单侧大小和每个位置最多保留的候选数量,满足 $1\le L\le10000$、$1\le D\le64$、$0\le G\le\min(L,32)$、$0\le W\le128$、$1\le T\le\min(L,16)$。

若 $G>0$,下一行包含 $G$ 个互不相同的整数 $p_{1},p_{2},\dots,p_{G}$,满足 $0\le p_{i}<L$,表示全局位置;若 $G=0$ 则不输入这一行,直接输入矩阵 $Q$。

接下来 $L$ 行,每行 $D$ 个整数 $Q[i][j]$,表示矩阵 $Q$;随后同样格式的 $L$ 行表示矩阵 $K$,再 $L$ 行表示矩阵 $V$。三个矩阵的元素都在 $[-10,10]$ 内。

所有矩阵和序列下标均从 $0$ 开始,全局位置的输入顺序不影响计算结果。保证所有中间结果和最终结果均在 64 位有符号整数范围内。

输出描述

输出 $L$ 行,每行 $D$ 个整数,表示最终矩阵 $O$。同一行中的整数之间用一个空格分隔。

样例 1

输入

4 2 1 1 2
2
1 0
1 0
1 0
1 0
1 0
2 0
3 0
4 0
1 10
2 20
3 30
4 40

输出

13 130
13 130
25 250
25 250

样例解释:全局位置集合为 $P={2}$。

对 $i=0$,$C_{0}={0,1,2}$,三个位置的分数分别为 $1,2,3$。由于 $T=2$,选择位置 $2$ 和位置 $1$:

\[O[0]=3\times[3,30]+2\times[2,20]=[13,130]\]

对全局位置 $i=2$,$C_{2}={0,1,2,3}$,分数最大的两个位置是 $3$ 和 $2$:

\[O[2]=4\times[4,40]+3\times[3,30]=[25,250]\]

样例 2

输入

3 1 0 2 1
1
1
1
2
2
1
10
20
30

输出

20
20
20

样例解释:本例 $G=0$,不存在全局位置。对任意位置 $i$,局部窗口覆盖整个序列,三个位置的分数分别为 $2,2,1$。由于 $T=1$,只保留分数最高的那一个候选;$j=0$ 与 $j=1$ 分数并列最高,按下标较小的优先,取 $j=0$,于是 $O[i]=2\times10=20$。

思路分析

第一步:圈出每个位置能看的地方。 全局位置看全部 $L$ 个下标;普通位置看窗口 $[\max(0,i-W),\min(L-1,i+W)]$,再补上落在窗口之外的全局位置。全局位置若本来就在窗口里,补的时候跳过,这就保证了「同一位置只计算一次」。

第二步:打分并筛出候选。 对每个候选 $j$ 用 $D$ 维点积算出 $\operatorname{score}(i,j)$,丢掉 $\operatorname{score}(i,j)\le0$ 的,剩下的按「分数降序、同分下标升序」排序后取前 $\min(T,\operatorname{cnt})$ 个,其中 $\operatorname{cnt}$ 是正分候选的个数。$\operatorname{cnt}=0$ 时这一行不累加任何东西,自然输出全 $0$。

第三步:加权求和。 把选中的项按 $O[i][x]=\sum_{j\in A_{i}}\operatorname{score}(i,j)\cdot V[j][x]$ 累加,得到第 $i$ 行的 $D$ 个输出值。

第四步:实现要点。 全量注意力的打分是 $O(L^{2}D)$,而候选集把每个普通位置的候选压到 $2W+1+G$ 个以内,全局位置才需要看整段序列。Python 的纯循环点积在最大数据上要跑上亿次乘法,因此每行只把点积交给 NumPy:取出候选的 $K$ 行与 $Q[i]$ 相乘,一次得到全部分数;排序用 np.lexsort,以分数降序为主键、下标升序为次键。单个分数最大为 $64\times100$,再乘 $V$ 并按 $T$ 项累加,仍远小于 $2^{63}$,用 64 位整数不会溢出。

正确性说明

候选集的构造与题面定义逐条对应:全局位置取全部下标,普通位置取窗口内下标并补上窗口外的全局位置,且同一位置只出现一次;窗口两端按 $0$ 与 $L-1$ 截断。打分只保留严格正分的候选,再按「分数降序、同分下标升序」取不超过 $T$ 个,与题面的筛选规则一致;候选不足 $T$ 个时切片自动全部保留。最后按定义式把 $\operatorname{score}(i,j)$ 与 $V[j]$ 加权求和,空集时输出全 $0$。三处都与定义一致,因此输出矩阵与题面要求完全相同。

题解代码

import sys

import numpy as np


# 构造位置 i 的候选集:全局位置取全部下标,其余位置取窗口并补上窗口外的全局位置
def build_candidates(i, length, window, is_global, global_list):
    # 全局位置能看整个序列,候选就是 0..L-1
    if is_global[i]:
        return np.arange(length)
    # 窗口在两端被截断到 [0, L-1]
    lo = max(0, i - window)
    hi = min(length - 1, i + window)
    cand = list(range(lo, hi + 1))
    # 已在窗口里的全局位置跳过,保证同一位置只算一次
    for p in global_list:
        if p < lo or p > hi:
            cand.append(p)
    return np.array(cand)


# 对候选打分,取前 T 个正分位置,加权求和得到输出行
def attend_row(cand, query, keys, values, top_t, dim):
    # 一次矩阵乘法算出全部候选的点积:(m, D) @ (D,) -> (m,)
    scores = keys[cand] @ query
    # 只保留严格为正的分数
    keep = scores > 0
    cand = cand[keep]
    scores = scores[keep]
    # lexsort 以最后一个键为主键:先按分数降序,同分再按下标升序
    order = np.lexsort((cand, -scores))[:top_t]
    # 一个有效候选都没有时,这一行输出全 0
    if order.size == 0:
        return np.zeros(dim, dtype=np.int64)
    # 有效候选不足 T 个时,切片自动全部保留
    return scores[order] @ values[cand[order]]


def solve():
    data = sys.stdin.buffer.read().split()
    pos = 0
    length, dim, global_count, window, top_t = (int(v) for v in data[pos:pos + 5])
    pos += 5
    global_list = []
    # G = 0 时没有全局位置这一行,按 G 决定是否读取
    if global_count > 0:
        global_list = [int(v) for v in data[pos:pos + global_count]]
        pos += global_count
    is_global = [False] * length
    for p in global_list:
        is_global[p] = True
    size = length * dim
    # 依次读入 Q、K、V 三个 L x D 矩阵,用 int64 防止累加溢出
    flat = np.fromiter((int(v) for v in data[pos:pos + 3 * size]),
                       dtype=np.int64, count=3 * size)
    q_mat = flat[:size].reshape(length, dim)
    k_mat = flat[size:2 * size].reshape(length, dim)
    v_mat = flat[2 * size:].reshape(length, dim)

    # 每个位置独立计算:建候选、打分排序取前 T 个、加权求和,逐行输出
    lines = []
    for i in range(length):
        cand = build_candidates(i, length, window, is_global, global_list)
        row = attend_row(cand, q_mat[i], k_mat, v_mat, top_t, dim)
        lines.append(" ".join(str(int(x)) for x in row.tolist()))
    sys.stdout.write("\n".join(lines) + "\n")


solve()

复杂度分析

时间复杂度:$O((L\cdot(2W+1+G)+G\cdot L)\cdot D)$。瓶颈是打分:普通位置至多 $2W+1+G$ 个候选,全局位置有 $L$ 个候选,每个候选要做一次 $D$ 维点积;排序的元素个数比打分少一个因子 $D$。

空间复杂度:$O(L\cdot D)$,存下三个矩阵;每行的候选与分数另占 $O(L)$,可以随用随弃。

易错点

  • 只保留严格大于 $0$ 的分数,$0$ 分候选必须剔除;剔除后一个都不剩时输出全 $0$ 行。
  • 候选集要去重:全局位置落在局部窗口内时不能重复计入,否则同一位置的分数会被算两次。
  • 排序口径是「分数降序、同分下标升序」,两次比较不能写反。
  • $G=0$ 时输入里没有全局位置那一行,多读一行会把矩阵整体错位。
  • 输出是每行 $D$ 个整数、行内空格分隔,不要输出成 Python 的列表格式。

第 2 题:流水线并行最小化最大 Stage 负载

题目描述

某深度学习团队正在训练一个大型神经网络,需要将模型的 $N$ 个层划分到 $K$ 个 GPU 上进行流水线并行训练。为了保证流水线效率,需要让各 GPU 的负载尽可能均衡。

流水线并行:把模型按层切分为 $K$ 个部分,每个部分称为一个 Stage,分配给一个 GPU。训练时,多个微批次在流水线中接力执行:第一个微批次在 GPU0 完成前向计算后传递给 GPU1,同时 GPU0 开始处理第二个微批次,以此类推。

Stage 负载:一个 Stage 的负载定义为该 Stage 内所有层的前向传播时间之和。若 Stage $s$ 包含层 $l_{1},l_{1}+1,\dots,l_{2}$,则

\[\operatorname{Load}_{s}=f[l_{1}]+f[l_{1}+1]+\cdots+f[l_{2}]\]

其中 $f[i]$ 是第 $i$ 层的前向传播时间。

气泡问题:流水线执行过程中,如果某个 Stage 负载明显高于其他 Stage,其他 GPU 就需要等待它完成,产生空闲时间,称为「气泡」。最大负载决定了气泡的上界,因此需要最小化最大负载。

划分约束:每个 Stage 内的层编号必须连续;每层恰好属于一个 Stage;每个 Stage 至少包含一层。

输入描述

第一行两个整数 $N,K$,分别表示模型的层数和划分的 Stage 数,满足 $1\le K\le N\le500$。

第二行 $N$ 个整数 $f[0],f[1],\dots,f[N-1]$,$f[i]$ 表示第 $i$ 层的前向传播时间(毫秒),满足 $1\le f[i]\le1000$。

输出描述

输出只有一行,包含一个整数,即最小化的最大 Stage 负载。

样例 1

输入

8 3
3 5 2 7 4 6 3 4

输出

13

样例解释:若干种划分方案如下,最优的最大负载为 $13$。

方案 Stage 0 Stage 1 Stage 2 最大负载
均分 [3,5,2] [7,4,6] [3,4] $\max(10,17,7)=17$
方案 A [3,5] [2,7,4] [6,3,4] $\max(8,13,13)=13$
方案 B [3,5,2] [7,4] [6,3,4] $\max(10,11,13)=13$

样例 2

输入

5 2
3 5 2 7 4

输出

11

样例解释:把 $5$ 层划分为 $2$ 个 Stage,每层耗时为 $[3,5,2,7,4]$,所有切法如下,最优方案 C 的最大负载为 $\max(10,11)=11$。

方案 Stage 0 Stage 1 负载分布 最大负载
A [3] [5,2,7,4] 3 vs 18 18
B [3,5] [2,7,4] 8 vs 13 13
C [3,5,2] [7,4] 10 vs 11 11
D [3,5,2,7] [4] 17 vs 4 17

思路分析

第一步:把问题翻译成区间划分。 题目要求把长度为 $N$ 的正整数序列切成恰好 $K$ 段连续非空的子段,让各段和的最大值尽量小。直接枚举切法要在 $N-1$ 个缝里选 $K-1$ 个,$N=500$ 时方案数是天文数字,不能枚举。

第二步:把求最优值转成判定问题。 给定一个上限 $\text{cap}$,「能不能切成不超过 $K$ 段、每段和都不超过 $\text{cap}$」很好判断,并且 $\text{cap}$ 越大需要的段数只会越少,判定结果单调,于是可以二分答案。

第三步:写 check。 从左往右贪心装层,当前段再装一层就超过 $\text{cap}$ 时就另起一段,得到最少段数 $\operatorname{cnt}(\text{cap})$。贪心之所以最少:每段都尽量往右装满,下一段的起点就尽可能靠右,剩下的层只会更少,不可能比别的切法多用段。可行条件是

\[\operatorname{cnt}(\text{cap})\le K\]

段数少于 $K$ 也算可行,因为 $N\ge K$,总能把某个含两层以上的段再拆开,直到恰好 $K$ 段;拆开后每段负载只减不增,仍不超过 $\text{cap}$。

第四步:定二分边界。 下界取 $\max f[i]$,最重的一层单独成段也得装下;上界取 $\sum f[i]$,所有层放进一段。若 $\operatorname{cnt}(\text{mid})\le K$ 说明 $\text{mid}$ 可行,答案不会更大,令 $\text{hi}=\text{mid}$;否则段数不够用,令 $\text{lo}=\text{mid}+1$。收敛时 $\text{lo}=\text{hi}$ 就是最小的可行上限,它恰好覆盖 $K=N$(每层单独一段,答案为 $\max f[i]$)与 $K=1$(答案为 $\sum f[i]$)两个边界。

正确性说明

对任意 $\text{cap}$,贪心扫描给出的段数不超过任何合法划分的段数:若某划分每段和都不超过 $\text{cap}$,把贪心得到的第 $j$ 段与最优划分的第 $j$ 段比较可知贪心段的右端点不会更靠左,归纳即得贪心段数最少。因此当且仅当存在合法划分时 $\operatorname{cnt}(\text{cap})\le K$,二分所需的单调性也由「$\text{cap}$ 增大时贪心段数不增」保证。二分取到的是最小的可行 $\text{cap}$,即最小的最大 Stage 负载。

题解代码

import sys

input = sys.stdin.readline


# 给定每段负载上限 cap,从左往右贪心装层,返回最少要切成几段
def count_stages(f, cap):
    stages = 1  # 开头就有第一段
    load = 0  # 当前这一段已装入的负载
    for x in f:
        # 再装这一层就超上限,只能让它另起一段;cap >= max(f) 保证单层一定装得下
        if load + x > cap:
            stages += 1
            load = 0
        load += x
    return stages


# 二分最大负载:cap 越大,贪心需要的段数越少,判定结果单调
def min_max_load(f, k):
    lo = max(f)  # 下界:最大的那一层必须装得下
    hi = sum(f)  # 上界:所有层放进同一段
    while lo < hi:
        mid = (lo + hi) // 2
        # 段数 <= k 就算可行:n >= k 时总能把含两层以上的段继续拆开,
        # 直到恰好 k 段且都非空;拆开后每段负载只会变小,仍不超过 mid
        if count_stages(f, mid) <= k:
            hi = mid
        else:
            lo = mid + 1
    return lo


def solve():
    n, k = map(int, input().split())
    f = list(map(int, input().split()))
    print(min_max_load(f, k))


solve()

复杂度分析

时间复杂度:$O(N\log\sum f)$。$\sum f\le5\times10^{5}$,二分约 $19$ 轮,每轮贪心扫描一遍为 $O(N)$;写成 $O(N^{2}K)$ 的区间划分 DP,在 $N=500$ 时约需 $1.25\times10^{8}$ 次转移,反而慢得多。

空间复杂度:$O(N)$,存下每层的前向传播时间,判定过程只用常数个变量。

易错点

  • 判定要允许段数少于 $K$,因为多出来的段总能继续拆分;写成「必须恰好等于 $K$」会漏掉可行解。
  • 二分下界必须是 $\max f[i]$,否则最重的那一层单独成段都会超上限,判定永远不可行。
  • 贪心装层时要把整层装进某一段,不能把一层切开分给两个 Stage,划分的连续性约束也要求切点只能落在层与层之间。
  • $K=N$ 与 $K=1$ 是两个边界,用 $\max f[i]$ 和 $\sum f[i]$ 作初值即可自然覆盖,不需要特判。

小结

  • 可见选择题覆盖了正态分布经验法则、推理与训练前向的差异、RoPE 的原理、单样本 $Z$ 检验和 CUDA 线程与共享内存;缺失的 15 题不作推测。
  • 稀疏局部注意力题没有需要发明的算法,比拼的是把候选集去重、正分筛选、平局排序和空集输出这些规则一条不落地实现出来。
  • 流水线划分题是「最小化最大值」的标准二分答案,把「段数不超过 $K$ 即可行」这一点想清楚就不会卡住。