大厂真题 / 华为

华为 8.19 笔试真题 - AI 岗

本场考试概述

考试时间:2026 年 8 月 19 日
考试岗位:AI 岗
难度评级:中等

考点分析

  1. 选择题(20 道):高维几何、概率统计、机器学习、RNN、混合精度、Transformer、FlashAttention、PagedAttention、MoE 与 RoPE。
  2. 流水线并行最小瓶颈:二分答案 + 贪心可行性检查。
  3. 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)$。

易错点

  1. 二分下界必须至少为最大单层计算量。
  2. 可行条件是贪心得到的最少段数 <= k,不是必须直接等于 k
  3. 上述“继续拆段”的论证依赖 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)$。

易错点

  1. 必须先减去公共最大值再取指数,避免上溢。
  2. 区间查询应保持块的原始顺序。
  3. 输出的是合并后的 v / d,不是三元组本身。
  4. 使用 None 作为单位元比猜测一个足够小的有限哨兵更稳健。