科大讯飞 AI Infra 校招 一面
Q: BF16 和 FP16 的区别?为什么大模型训练用 BF16?
| 属性 | FP16 | BF16 |
|---|---|---|
| 符号位 | 1 | 1 |
| 指数位 | 5 | 8 |
| 尾数位 | 10 | 7 |
| 动态范围 | ±6.5×10⁴ | ±3.4×10³⁸(与 FP32 相同) |
| 精度(最小精度差) | ~0.001 | ~0.01 |
| 特殊值 | 有 Inf/NaN | 有 Inf/NaN |
为什么大模型训练用 BF16:
动态范围是关键:训练中梯度和 loss 值可能非常大(>65504 溢出 FP16 的上限)或非常小(<6e-8 下溢为 0),BF16 的 8 位指数与 FP32 动态范围一致(±3.4×10³⁸),完全避免溢出问题
无需 Loss Scaling:FP16 训练必须配合 loss scaling(放大 loss 防止梯度下溢,更新前再缩小)。BF16 动态范围足够,可以直接 drop-in 替换 FP32,无需额外处理
精度损失可接受:虽然 BF16 尾数只有 7 位(精度比 FP16 低),但大模型训练中重要的是梯度方向而非绝对精度。实验表明 BF16 训练精度与 FP32 几乎一致
硬件支持:A100+ 的 Tensor Core 原生支持 BF16,吞吐与 FP16 相同
典型混合精度策略:
- 主权重(master weight):FP32(Adam 优化器状态中)
- 前向/反向计算:BF16
- 梯度 AllReduce:BF16
- 这样只需 2 bytes/param 的通信和计算,但保持 FP32 级别的训练稳定性
Q: 大端和小端的区别?主流架构用什么?
大端(Big-Endian):高字节在低地址。数据在内存中的顺序与人类阅读顺序一致。
1 | 0x12345678 → 内存低地址到高地址:[12][34][56][78] |
小端(Little-Endian):低字节在低地址。便于硬件实现(从低地址开始读就是最低有效位)。
1 | 0x12345678 → 内存低地址到高地址:[78][56][34][12] |
主流架构的字节序:
- x86/x64(Intel/AMD):小端——PC 和服务器的绝对主流
- ARM:默认小端(Bi-endian,可配置为大端,但实际几乎都用小端)
- RISC-V:小端
- 网络协议(TCP/IP):大端(网络字节序),因此跨网络传输时需要
htonl()/ntohl()转换 - POWER(IBM):大端(旧版),Power9+ 支持小端
为什么需要关注:
- 网络编程中必须处理字节序转换
- 序列化/反序列化二进制数据时需要一致
- GPU 通信中(NCCL/RDMA)也需注意,不过 NVIDIA GPU 与 x86 一样使用小端
Q: 如果设计一个 CPU,从哪几个部分考虑?
设计 CPU 需要从微架构的多个维度进行权衡:
1. 指令集架构(ISA):
- RISC vs CISC:RISC 指令定长、译码简单、利于流水线(ARM/RISC-V);CISC 指令变长但代码密度高(x86)
- 指令格式、寻址模式、寄存器数量(通用寄存器越多减少访存)
2. 流水线设计:
- 经典五级:取指(IF)→ 译码(ID)→ 执行(EX)→ 访存(MEM)→ 写回(WB)
- 现代高性能 CPU 流水线 12-20 级(更深 = 更高频率,但分支惩罚更大)
- 数据冒险处理:Forwarding/Bypassing
3. 分支预测:
- 现代 CPU 95%+ 预测准确率(TAGE 预测器、Neural Branch Predictor)
- BTB(Branch Target Buffer)缓存跳转目标地址
- 预测失败惩罚 = 流水线深度 × cycle(深流水线代价更大)
4. 缓存层次:
- L1(32-64 KB,1-4 cycles)→ L2(256KB-1MB,10-20 cycles)→ L3(数十 MB,30-50 cycles)
- 关联度(way)、替换策略(LRU/随机)、一致性协议(MESI/MOESI)
5. 乱序执行(Out-of-Order):
- Tomasulo 算法 + 保留站(Reservation Station)实现指令动态调度
- ROB(Reorder Buffer)保证指令顺序退休
- 允许独立指令提前执行,隐藏 cache miss 等延迟
6. 多核/超标量:
- 超标量:每 cycle 发射多条指令(如 4-wide、6-wide)
- 多核:多个独立执行核心,共享 L3 和内存
- SMT(超线程):一个物理核虚拟为 2 个逻辑核,共享执行资源
Q: 用户态和内核态的区别?如何切换?
本质区别——CPU 权限级别不同:
用户态(Ring 3):程序运行在受限权限下,不能直接访问硬件(I/O 端口、MMU)、不能执行特权指令(如关中断
cli、修改页表mov cr3)、只能访问自己的虚拟地址空间内核态(Ring 0):操作系统内核运行,拥有完全权限,可访问所有物理内存、操控硬件、管理进程调度
切换方式(用户态 → 内核态):
系统调用(Syscall)——主动:
- 用户程序通过
syscall指令(x64)或int 0x80(x86)发起 - 如
read()、write()、mmap()等 - CPU 切换到 Ring 0,跳转到内核的 syscall handler
- 用户程序通过
中断(Interrupt)——被动:
- 硬件中断:时钟中断(触发调度)、网卡/磁盘 I/O 完成通知
- CPU 自动保存上下文并跳转到中断服务程序
异常(Exception)——被动:
- 缺页异常(Page Fault):访问未映射的虚拟地址
- 除零错误、非法指令、段错误等
- CPU 陷入内核处理异常
切换过程开销:
- 保存用户态寄存器上下文(压栈)
- 切换栈指针到内核栈
- 刷新 TLB(如果需要切换页表)
- 典型开销:数百纳秒到数微秒
- 频繁 syscall 是性能瓶颈——现代方案:io_uring(减少 syscall 次数)、vDSO(纯用户态读时钟)
Q: NPU 开发的难点和策略?
难点分析:
软件生态不成熟:
- CUDA 经过 15+ 年迭代,有 cuDNN/cuBLAS/NCCL 等完善的库;NPU 的等效库覆盖不足
- 调试工具不完善:缺少类似 compute-sanitizer/Nsight Compute 的成熟工具
- 文档和社区资源少
算子库覆盖不足:
- LLM 中的长尾算子(RoPE、SwiGLU、各种 Attention 变体)需要手写适配
- 不同 NPU 的编程模型差异大(华为 Ascend C、寒武纪 BANG C、摩尔线程 MUSA)
内存模型差异:
- NPU 的片上存储层次与 GPU 不同(如华为的 AI Core 有 L0A/L0B/L1/UB 多级 buffer)
- 数据搬运需要显式编程(类似 DMA),不像 GPU 有硬件 cache
性能调优困难:
- Profiling 工具不完善,很多行为是黑盒
- 性能计数器不如 NVIDIA 丰富
- Roofline 分析缺少精确的硬件参数
应对策略:
- 算子迁移:优先对齐 CUDA 算子语义(保持输入输出一致),逐步迁移验证
- 利用厂商高性能库:如华为 CANN(包含 nn 算子库、图优化引擎)
- 图层面优化:最大化算子融合减少数据搬运——在 NPU 上搬运开销比 GPU 更显著
- 编译器路线:用 TVM/Triton-like 编译器生成 kernel,减少手写工作
- 紧密协作:与硬件团队获取底层性能信息、micro-benchmark 结果
Q: Softmax 优化中如何解决负载不均衡?
Softmax 计算 softmax(x)_i = exp(x_i - max(x)) / Σ exp(x_j - max(x)) 涉及 reduce(求 max、求 sum)和 elementwise(exp、div)操作,负载不均衡主要出现在以下场景:
不均衡来源:
- 不同 token 的实际序列长度不同(padding 场景)
- Attention Softmax 中不同 Q token 对应不同 K 长度(因果 mask 下越靠前的 token K 越少)
- Batch 内不同样本序列长度差异大
解决方案:
动态分配:按实际有效长度分配计算任务,对 padding 部分不做计算。实现上通过传入
valid_length数组,每个线程只处理有效范围分块处理(Tiling):将长序列切分为固定大小 block(如 256),每个 thread block 处理一个 block。这样即使序列长度不同,每个 block 的工作量一致
Online Softmax:分块计算时维护 running max/sum,避免全局归约的同步等待:
1
2m_new = max(m_old, block_max)
l_new = l_old * exp(m_old - m_new) + block_sum * exp(block_max - m_new)Warp 级归约:利用
__shfl_down_sync在 warp(32线程)内做 reduce,避免 shared memory 同步的开销。对于短序列(≤32 元素),一个 warp 即可完成Persistent Kernel:对于 batch 中序列长度差异大的情况,使用 persistent kernel + 任务队列,每个 block 处理完当前任务后从队列取下一个,天然负载均衡
Q: Tensor Parallel 切分的是什么?涉及哪些通信?
切分内容:TP 切分的是模型线性层的权重矩阵,将大矩阵按行或列切分到多个 GPU 上,每张卡执行部分矩阵乘法。
具体切法(以 LLaMA 的 Transformer MLP 为例):
1 | 输入 x: [batch, seq, hidden] |
通信操作总结:
| 操作 | 通信类型 | 通信量 | 触发位置 |
|---|---|---|---|
| Column Parallel → Row Parallel | 无通信 | 0 | 中间无需同步 |
| Row Parallel 输出 | AllReduce | 2×batch×seq×hidden×dtype | 每层 MLP 结束 |
| Attention O_proj 输出 | AllReduce | 2×batch×seq×hidden×dtype | 每层 Attention 结束 |
每个 Transformer 层的前向需要 2 次 AllReduce(Attention 和 MLP 各一次)。
优化——Sequence Parallel:
- 用 ReduceScatter + AllGather 替代 AllReduce
- 将 LayerNorm/Dropout 沿序列维度分片(每卡处理部分 token 的 norm)
- 通信量不变,但可以与计算重叠,且 norm 计算也被分摊