荣耀 AI Infra 校招 二面
Q: LLM 在 NPU 上的最大瓶颈在哪?
NPU(如华为昇腾 Ascend、寒武纪 MLU、海光 DCU)相比 NVIDIA GPU 生态在部署 LLM 时面临多层次瓶颈:
1. 内存带宽瓶颈(最核心):
LLM Decode 阶段是 memory-bound——每步需读取全部模型权重:
1 | Decode 每步数据读取: model_params × bytes_per_param |
NPU 的 HBM 带宽通常低于同代 NVIDIA GPU(H100: 3.35 TB/s vs Ascend 910B: ~1.6 TB/s),这在 decode 场景下直接限制了单卡吞吐。
2. 算子生态和适配问题:
| 算子 | CUDA 生态 | NPU 生态 | 差距原因 |
|---|---|---|---|
| GEMM | cuBLAS (极致优化) | 自研 GEMM library | 优化迭代时间短 |
| FlashAttention | 原生支持 (FA1/2/3) | 需要重新适配 AI Core | 片上 buffer 架构不同 |
| RoPE | Triton/CUDA 实现丰富 | 需要手写 TIK/Ascend C | 开发者少 |
| Fused LayerNorm | 多种开源实现 | 需要自研 | 融合模式不同 |
| SwiGLU | 简单融合 kernel | 需要验证算子兼容性 | 算子库覆盖度低 |
LLM 有很多特殊算子组合(如 RoPE + GQA Attention + SwiGLU),需要针对 NPU 架构重新设计高效实现。
3. 动态 shape 处理:
- LLM 推理的 seq_len 和 batch_size 是动态变化的(Continuous Batching)
- NPU 编译器通常偏好静态 shape(可做更多编译期优化)
- 动态 shape 可能导致:频繁重编译、无法使用最优 tiling、算子选择不优
4. KV Cache 管理灵活性:
- PagedAttention 需要间接寻址(通过 block table 索引 KV blocks)
- NPU 的内存管理 API 可能不如 CUDA 灵活(如自定义 memory allocator)
- 分页管理的 kernel 需要重新适配 NPU 的存取模式
5. 软件栈成熟度:
1 | NVIDIA 推理栈: NPU 推理栈: |
实际应对策略:
- 优先做 GEMM kernel 极致优化(占时间 70%+)
- 将 FlashAttention 适配 NPU 的片上 buffer(类比 shared memory)
- 实现类 PagedAttention 的 KV Cache 分页管理
- 利用 NPU 的特有优势(如某些 NPU 有更大的片上缓存)
Q: Prefill 和 Decoding 的 Matmul 优化方法有什么不同?
两个阶段的矩阵乘法有本质不同的形状特征和瓶颈类型,需要完全不同的优化策略:
Prefill 阶段——大矩阵乘(Compute-bound):
1 | 形状: [batch × seq_len, hidden] × [hidden, hidden] |
Prefill Matmul 优化重点:
| 优化方向 | 具体方法 | 效果 |
|---|---|---|
| 最大化 Tensor Core 利用率 | MMA 指令 + 合适的 tile 大小 | 利用率 > 80% |
| 大 Tile + 高数据复用 | BM=BN=128-256, BK=32-64 | 减少全局访存 |
| 双缓冲预取 | 异步加载下一个 tile 到 shared memory | 隐藏加载延迟 |
| 最优 Block 配置 | 保证足够 occupancy 隐藏计算延迟 | SM 利用率高 |
| Pipeline(软流水) | 加载、计算、存储三级流水 | 最大化吞吐 |
| TMA (H100) | Tensor Memory Accelerator 异步加载 | 释放计算 SM |
Decode 阶段——瘦长矩阵乘/GEMV(Memory-bound):
1 | 形状: [batch × 1, hidden] × [hidden, hidden] |
Decode Matmul 优化重点:
| 优化方向 | 具体方法 | 效果 |
|---|---|---|
| 减少权重读取量 | INT4/INT8 量化(W4A16/W8A8) | 带宽需求减少 2-4x |
| 向量化访存 | float4/LDG.128 加载 | 减少内存事务数 |
| Split-K 并行 | 沿 K 维切分增加并行度 | 更多 SM 参与(M 小时 SM 不够饱和) |
| 增大 batch | Continuous Batching 凑大 batch | 提高 AI,从 memory→compute bound 过渡 |
| Persistent Kernel | 一个 kernel 处理多层 | 减少 launch 开销 |
| 权重预排列 (Packing) | 按 tile 顺序重排权重 | 连续内存访问 |
Split-K 策略详解:
1 | 常规 GEMM (M=1, K=4096, N=4096): |
关键差异总结:
| 维度 | Prefill (大 GEMM) | Decode (GEMV/瘦 GEMM) |
|---|---|---|
| 瓶颈 | Compute-bound | Memory-bound |
| 核心优化 | 最大化 TC 利用率 | 最大化带宽利用率 |
| Tiling 策略 | 大 tile 高复用 | 小 tile + Split-K |
| 量化收益 | FP8 加速计算(TC 吞吐 2x) | INT4 减少读取量(带宽收益 4x) |
| Batch 影响 | batch 越大越好(更大 M) | batch 增大可过渡到 compute-bound |
Q: 分块策略如何保证数据在缓存中的连续性?
问题本质:Tiling 将大矩阵分块后,每个 tile 在原始矩阵中可能不是内存连续的(如行优先存储时列方向的 tile)。需要确保加载到片上缓存后的访问是高效的。
1. 数据布局对齐(最基础):
1 | 行优先 (Row-major) 存储: |
2. Block 大小对齐缓存行:
1 | GPU 的 Global Memory 事务: 32 bytes (L2) 或 128 bytes (L1) |
3. DMA/TMA 预取策略:
1 | 在 NPU/H100 上: |
4. 避免片上缓存的 Bank Conflict:
1 | Shared Memory 加载后的布局: |
**5. 权重预排列 (Weight Pre-packing)**:
1 | 推理前一次性重排权重: |
6. NPU 特殊考虑:
- NPU 的片上 buffer 通常比 GPU shared memory 大(如 Ascend AI Core 有 ~MB 级 Local Memory)
- 更大的片上缓存意味着可以存更大的 tile → 更少的全局访存次数
- 但 DMA 传输与计算的并行需要手动编排(PIPE_M/PIPE_V 分离)
Q: 昇腾 NPU 架构对 Transformer 友好吗?
昇腾 NPU(如 Ascend 910B)的核心架构:
1 | Ascend 910B: |
对 Transformer 友好的方面:
| 维度 | 友好原因 | 对应 Transformer 操作 |
|---|---|---|
| Cube Unit | 专为矩阵乘设计,FP16/INT8 高吞吐 | Q×K^T, Attn×V, FFN 的 Linear |
| 大 Local Memory | 可存更大的 tile(~MB 级 vs GPU ~KB 级) | FlashAttention 式 tiling 可用更大 block |
| 多核并行 | 32 AI Core 并行 | 多 head 并行、batch 并行 |
| AI Core 内流水 | Cube 和 Vector 可以流水执行 | GEMM + activation 可以 pipeline |
不够友好的方面:
| 维度 | 具体问题 | 影响的操作 |
|---|---|---|
| 编程模型 | TIK/Ascend C 不如 CUDA 灵活 | 自定义算子开发慢 |
| 动态 shape | 编译器优化依赖静态 shape | Continuous Batching 的动态 seq/batch |
| 非 GEMM 算子 | Vector Unit 性能/工具不如 CUDA Core | Softmax, RoPE, LayerNorm |
| 生态 | 缺少 FlashAttention 等成熟实现 | 需要从头适配 |
| Profiling | 工具不如 Nsight 成熟 | 性能瓶颈定位困难 |
| 量化支持 | INT4/FP8 支持可能不完善 | 低精度推理加速受限 |
实际适配建议:
1 | 优先适配: |
Q: 量化后的大模型运行时内存占用大概多少?
模型权重内存(最确定的部分):
| 模型规模 | FP16 (2B/param) | INT8 (1B/param) | INT4 (0.5B/param) |
|---|---|---|---|
| 1.5B | 3 GB | 1.5 GB | 0.75 GB |
| 7B | 14 GB | 7 GB | 3.5 GB |
| 13B | 26 GB | 13 GB | 6.5 GB |
| 70B | 140 GB | 70 GB | 35 GB |
| 405B | 810 GB | 405 GB | 202 GB |
注:INT4 量化还需要额外存储 scale/zero_point(每 group 128 个参数存一组 FP16 scale),额外增加约 0.5-2%。
KV Cache 内存(动态部分,取决于 batch/seq):
1 | KV Cache = 2 × num_layers × num_kv_heads × head_dim × seq_len × batch_size × bytes |
其他内存开销:
| 组件 | 大小 | 说明 |
|---|---|---|
| CUDA/框架运行时 | 1-2 GB | CUDA context、cuBLAS workspace |
| 激活值 (前向计算中间结果) | 0.5-2 GB | 取决于 batch 大小和算子融合程度 |
| 量化 scale/metadata | 模型的 0.5-2% | per-group scale + zero_point |
| Block Table (PagedAttention) | < 100 MB | 请求数 × max_blocks × sizeof(int) |
总内存估算公式:
1 | 总显存 ≈ 模型权重 + KV Cache + 框架开销 + 激活缓冲 |
实际部署的显存规划建议:
- 预留 10-20% 显存余量给内存碎片和突发峰值
- KV Cache 用 PagedAttention 按需分配,避免预分配最大值
- 量化 KV Cache(FP8)可以在大 batch 场景下翻倍并发量