字节跳动 AML AI Infra 一二面
Q: LLM推理有什么主要瓶颈?
LLM推理的瓶颈本质源于自回归生成的计算模式和巨大的模型参数量,具体表现为以下五个层面:
1. 内存带宽瓶颈(Decode阶段核心瓶颈):
Decode阶段每步生成1个token,却需要读取全部模型权重:
1 | 计算量: 2 × Params × 1(token) ≈ 140 GFLOP (70B模型) |
这就是为什么Decode阶段是memory-bound——算力远远富余,带宽成为瓶颈。单步延迟 ≈ 模型大小 / HBM带宽:
- 7B FP16: 14GB / 2TB/s = 7ms
- 70B FP16: 140GB / 2TB/s = 70ms(单卡不可接受,需要TP)
2. KV Cache显存限制:
| 模型 | 每token KV大小 | 4K序列单请求 | 32并发4K | 占比(80GB) |
|---|---|---|---|---|
| LLaMA-7B | 0.5MB | 2GB | 64GB | 80% |
| LLaMA-70B | 1.25MB | 5GB | 160GB | 200%! |
| Qwen-72B(GQA) | 0.33MB | 1.3GB | 42GB | 53% |
KV Cache显存随seq_len×batch_size线性增长,成为限制最大并发数和最大序列长度的硬约束。GQA通过减少KV heads缓解(8→1),但长序列场景下仍然是瓶颈。
3. 自回归串行限制:
- 每个token的生成依赖前一个token的输出(因为需要前一个token的hidden state来计算attention)。
- 无法像Prefill阶段那样并行处理所有token。
- 生成N个token至少需要N步串行推理,延迟 = N × 单步延迟。
- 即使硬件无限强(单步0延迟),也需要N步逻辑串行。
- 这是自回归模型的结构性限制,只能通过投机解码/非自回归方法缓解。
4. Prefill计算延迟:
1 | Prefill计算量 = 2 × Params × seq_len(GEMM部分) |
长输入的首token延迟(TTFT)高,直接影响用户体验。Attention的O(n^2)复杂度使得超长序列(>32K)的Prefill变得极其昂贵。
5. 调度效率低:
- 不同请求长度差异大(从10 tokens到32K tokens)。
- 静态batching中短请求等待长请求完成→padding浪费。
- Prefill和Decode对计算资源的需求不同(compute-bound vs memory-bound),混合调度导致两者都不最优。
- 请求到达时间不均匀,burst到达时排队延迟增加。
各瓶颈的量化影响排序(70B模型推理):
| 瓶颈 | 影响程度 | 核心指标 | 优化后收益 |
|---|---|---|---|
| 带宽瓶颈 | 最高 | 单步70ms(FP16单卡) | 量化+TP→10ms |
| KV Cache | 高 | 限制并发32→满载 | PagedAttention→3x并发 |
| 串行生成 | 高 | 延迟=N×单步 | 投机解码→延迟/2-3 |
| Prefill延迟 | 中 | TTFT 3+秒 | PD分离+FlashAttn→1秒 |
| 调度浪费 | 中 | GPU利用率30% | Continuous Batch→75% |
Q: LLM推理主要的优化技术?
每种优化技术针对特定瓶颈,下面按原理和收益详细展开:
1. KV Cache + PagedAttention:
- KV Cache消除重复计算(从O(n^2)到O(n) per step)。
- PagedAttention解决显存碎片问题(利用率从~40%提升到>95%)。
- 两者组合效果:同样显存支持3-4x更多并发请求。
2. FlashAttention减少HBM访问:
1 | 标准Attention: Q×K^T(写HBM) → Softmax(读写HBM) → ×V(写HBM) |
- Prefill阶段加速2-4x(IO密集时更显著)。
- 额外收益:显存省N^2(不需要存完整attention矩阵)。
- FlashDecoding:Decode阶段在KV维度并行,利用更多SM资源。
3. Continuous Batching动态调度:
1 | 静态Batching: |
- 吞吐提升2-3x,平均延迟降低(短请求不再被长请求阻塞)。
- 实现需要细粒度的内存管理(每个请求独立的KV Cache管理)。
4. 量化减少带宽需求:
| 量化方案 | 带宽节省 | 精度损失 | 额外收益 |
|---|---|---|---|
| W8A8 (SmoothQuant) | 2x | <1% | INT8 TC吞吐2x |
| W4A16 (GPTQ/AWQ) | 4x(权重) | 1-3% | 单卡放更大模型 |
| FP8 (H100原生) | 2x | <0.5% | FP8 TC吞吐2x |
| W4A4 | 4x | 3-5% | 极限压缩 |
Decode阶段是memory-bound,量化直接将瓶颈带宽需求降低2-4x→接近等比例加速。
5. 投机解码减少生成步数:
1 | 传统: N tokens需要N步大模型推理 |
- 关键条件:小模型的延迟远小于大模型;接受率足够高。
- Medusa/EAGLE变体:不用独立小模型,用附加head预测多个token。
6. 张量并行多卡降低延迟:
- TP=N时,每卡只需存储和读取1/N的权重→单步延迟降为约1/N。
- 通信开销:每层2次AllReduce,NVLink(900GB/s)下通常<5%总延迟。
- 实际加速:TP=8时约5-6x(通信+同步开销),但延迟从70ms降至~12ms。
- 注意:TP增加batch维度不变→吞吐不变,纯粹降延迟。
7. PD分离独立优化各阶段:
| 阶段 | 特征 | 最优配置 |
|---|---|---|
| Prefill | Compute-bound, 大矩阵乘 | 高算力GPU, 大batch并行 |
| Decode | Memory-bound, 低算术强度 | 高带宽, 大batch以提高AI |
分离后:
- Prefill集群:可以充分利用Tensor Core算力。
- Decode集群:优化batch size以提高带宽利用率,不被Prefill抢占。
- 消除调度干扰:Prefill的burst计算不再阻塞Decode的延迟敏感步骤。
8. 算子融合减少kernel开销:
1 | 未融合: LayerNorm(读HBM→写HBM) + Residual(读HBM→写HBM) + Linear(读HBM→写HBM) |
优化技术组合的实际应用(以vLLM为例):
1 | vLLM = PagedAttention + Continuous Batching + FlashAttention + |
Q: PagedAttention的原理?
核心问题——传统KV Cache的显存碎片化:
1 | 传统方案——预分配连续显存: |
PagedAttention的设计——借鉴OS虚拟内存:
1 | 核心思想: 将KV Cache切分为固定大小的block(如16 tokens) |
分配流程:
1 | 1. 新请求到达: |
Copy-on-Write共享前缀:
1 | Prompt: "You are a helpful assistant. User: " |
Attention Kernel的适配:
1 | // PagedAttention kernel伪代码 |
性能数据对比:
| 指标 | 传统连续分配 | PagedAttention | 提升 |
|---|---|---|---|
| 显存利用率 | 20-40% | >95% | 2-4x |
| 最大并发(13B,A100) | ~8请求 | ~24请求 | 3x |
| 吞吐量 | baseline | 2-4x | 显著 |
| 内存碎片 | 严重 | 近零(仅最后block) | - |
| Beam Search显存 | K×full copy | 共享+CoW | K倍节省 |
Q: Orca迭代级请求调度是什么?
Orca(Continuous Batching / Iteration-level Scheduling)是Yu等人2022年提出的LLM推理调度策略,核心创新在于将调度粒度从”请求级别”细化到”单步迭代级别”:
传统静态Batching的问题:
1 | 时间线: |
Orca的迭代级调度:
1 | 时间线: |
技术实现细节:
逐步检查完成状态:每次decode迭代后检查每个请求是否生成了EOS token或达到max_length。完成的请求立即移出当前batch。
动态插入新请求:空出的slot立即从等待队列中取出优先级最高的新请求。新请求先做Prefill再加入Decode batch(或使用chunked prefill混合调度)。
变长序列处理:Batch内不同请求长度不同,需要特殊的Attention Kernel支持(如Paged Attention中通过block table处理不同长度)。
调度策略:
- FCFS(先来先服务):公平但可能P99延迟高。
- Shortest-Job-First:优先短请求,降低平均延迟。
- 优先级调度:VIP请求抢占普通请求。
与PagedAttention的协同:
1 | Orca需要: 高效的KV Cache插入/释放 |
Chunked Prefill(进一步优化):
1 | 问题: 长prompt的Prefill计算量大,会阻塞整个batch的Decode |
性能收益量化:
| 指标 | 静态Batching | Orca/Continuous Batching | 提升 |
|---|---|---|---|
| GPU利用率 | 30-50% | 70-90% | 2x+ |
| 吞吐量(tok/s) | baseline | 2-3x | 显著 |
| 平均延迟 | 高(等待长请求) | 低(短请求快速完成) | 30-50%↓ |
| P99延迟 | 极高 | 可控 | 大幅改善 |
| 显存效率 | 低(padding浪费) | 高(无padding) | 2x+ |
生产系统实现:
- vLLM:基于Orca + PagedAttention,最主流的开源推理引擎。
- TGI(Hugging Face):类似实现。
- TensorRT-LLM:NVIDIA官方,in-flight batching。
- SGLang:进一步优化RadixAttention实现更智能的调度。
Q: C++数组下标越界会报什么错?
C++数组越界是最经典的未定义行为(UB)之一——语言标准不规定任何特定行为,编译器不保证报错或崩溃:
栈上数组越界(最常见的隐蔽bug):
1 | void foo() { |
不同越界方式的典型表现:
| 越界类型 | 可能的现象 | 根因 |
|---|---|---|
| 栈上数组写越界 | 覆盖其他局部变量/返回地址 | 栈是连续内存,无边界保护 |
| 栈上数组读越界 | 读到垃圾值或其他变量 | 同上 |
| 堆数组写越界(小) | 覆盖堆元数据→后续free崩溃 | malloc header紧挨数据 |
| 堆数组写越界(大) | SIGSEGV(跨越页边界) | 触碰到未映射页 |
| 负索引越界 | 访问数组前方的内存 | 可能是其他变量或guard page |
为什么编译器不检查:
- C/C++的设计哲学:”不为不需要的东西付代价”。
- 每次数组访问都做范围检查→循环中的数组访问会慢2-5x(一次比较+一次分支)。
- 编译器可以在编译期检查常量索引的越界(但仅警告,不报错)。
检测方法(从快到慢):
| 工具 | 检测能力 | 运行时开销 | 使用方式 |
|---|---|---|---|
| -Wall -Wextra | 编译期常量越界警告 | 0 | 编译选项 |
| AddressSanitizer | 栈/堆/全局越界 | 2x时间, 2-3x内存 | -fsanitize=address |
| Valgrind(Memcheck) | 堆越界、UAF、泄漏 | 10-50x时间 | valgrind ./app |
| Bounds Checking | 精确范围检查 | 2-5x | -fsanitize=bounds |
| std::vector::at() | 运行时异常 | 极小 | 替代[]运算符 |
AddressSanitizer工作原理:
1 | 为每个内存区域维护"shadow memory"记录是否可访问: |
安全的替代方案:
1 | // 1. std::array + at() |
AI框架中的相关问题:
- CUDA kernel中的数组越界:不会立即崩溃,可能写坏其他数据→结果静默错误。需要
compute-sanitizer检测。 - Tensor shape不匹配:PyTorch/TensorFlow会做运行时shape检查,越界会抛异常。
- 内存访问越界是GPU kernel最常见的bug之一(尤其在手写kernel中)。
Q: Linux下如何Debug和定位错误?
Linux环境下的调试是一个系统化的过程,需要根据问题类型选择合适的工具组合:
1. GDB(最基础、最强大的交互式调试器):
1 | # 编译时加-g保留调试信息 |
适用场景:逻辑错误、崩溃定位、多线程死锁分析。
2. Core Dump分析(事后调试/生产环境):
1 | # 启用core dump |
关键优势:不需要重现问题——生产环境崩溃一次就能分析。
3. AddressSanitizer(内存错误首选检测工具):
1 | # 编译时启用 |
开销:约2x运行时间、3x内存。建议CI中始终开启。
4. strace(系统调用级别追踪):
1 | # 追踪程序的所有系统调用 |
适用场景:IO问题、权限问题、程序启动失败、动态库加载问题。
5. dmesg/journalctl(内核级别信息):
1 | # 查看内核消息(OOM、segfault等) |
6. Valgrind(最全面的内存检查,但最慢):
1 | # 内存错误检测 |
7. perf(性能分析和热点定位):
1 | # CPU热点分析 |
调试决策树——根据症状选工具:
| 症状 | 首选工具 | 备选 |
|---|---|---|
| Segfault崩溃 | Core dump + GDB bt | ASan重现 |
| 结果错误(静默bug) | ASan + GDB | Valgrind |
| 内存泄漏 | ASan(LeakSan) | Valgrind |
| 性能问题 | perf + 火焰图 | strace(IO) |
| 程序卡死 | GDB attach + bt | strace |
| 多线程死锁 | GDB thread apply all bt | ThreadSanitizer |
| OOM被杀 | dmesg + memory profiler | /proc/pid/status |
| 文件/网络问题 | strace | ltrace |
| CUDA错误 | compute-sanitizer | cuda-gdb |
AI Infra特有的调试场景:
- CUDA illegal memory access:用
compute-sanitizer --tool memcheck ./app定位kernel内的越界。 - NCCL通信挂起:
NCCL_DEBUG=INFO环境变量 + GDB attach查看各rank状态。 - PyTorch CUDA OOM:
torch.cuda.memory_summary()查看显存分配详情。 - 分布式训练某个rank挂住:
py-spy采样Python调用栈,不需要GDB。
Q: 手撕:反转链表?
(编程题)
Q: 手撕:LRU Cache?
(编程题)