腾讯 AI Infra 实习 一面 (1)
Q: Chunked Prefill是什么?它为了解决什么问题?
Chunked Prefill将长序列的prefill阶段分成多个固定大小的chunk(如512或1024 token)分批处理,而不是一次性计算整个序列。
解决的核心问题:
1. Prefill阻塞Decode(Head-of-Line Blocking):
1 | 传统方式: |
- 长序列prefill可能占用GPU数百毫秒(如5000 token的prefill约100-500ms)
- 期间所有decode请求被阻塞,导致已在生成的用户体验到卡顿
- 分chunk后每个chunk只占10-50ms,decode请求的等待时间大幅缩短
2. 显存峰值过高:
- Attention计算的临时空间与序列长度相关(非FlashAttention时为O(n^2))
- FlashAttention下虽然O(1)临时空间,但整个序列的KV-Cache一次性分配仍占用大量显存
- 分chunk后每次只需分配一个chunk大小的临时空间
实际效果(SarathiServe论文数据):
- P99 TPOT(Time Per Output Token)降低50-80%
- 代价:总prefill时间略增(分chunk间有调度开销)
- 整体吞吐基本不受影响
实现细节:
- Chunk之间的KV-Cache需要持久化(前一个chunk计算的KV存入cache)
- 后续chunk的Attention需要关注前面所有chunk的KV(cross-chunk attention)
- 调度器在每个iteration判断:当前step执行prefill chunk还是decode
Q: Reduce-Scatter和All-to-All通信的区别?
Reduce-Scatter:规约 + 分发
1 | 输入: 每个rank持有完整数据(如完整梯度) |
All-to-All:全交换
1 | 输入: 每个rank持有不同数据要发给不同目标 |
应用场景对比:
| 操作 | 应用场景 | 特点 |
|---|---|---|
| Reduce-Scatter | ZeRO梯度分片、TP行切后的输出分片 | 有规约操作(sum/avg) |
| All-to-All | MoE的token dispatch/combine、序列并行 | 纯数据重排,无计算 |
| AllReduce | DDP梯度同步 | = Reduce-Scatter + AllGather |
| AllGather | ZeRO3参数收集、TP列切后拼接 | 每rank持有部分,收集为完整 |
MoE中All-to-All的具体使用:
1 | Expert Parallelism中,每个rank负责不同expert: |
Q: 怎么减少Launch Kernel的开销?
每次kernel launch涉及CPU端的驱动调用和GPU端的任务分发,有固定开销约5-10微秒/次。当kernel本身很小(如element-wise操作处理少量数据)时,launch开销占比可能超过50%。
主要优化方法:
1. CUDA Graph(最有效,decode场景标配):
1 | // 录制阶段(一次性) |
- 将多次launch合并为一次graph launch
- CPU端开销从N×5us降为1×5us
- 限制:graph内的kernel参数和shape不能变化
2. 算子融合(从根本上减少kernel数量):
- 将ReLU+Add+Mul融合为一个kernel → 3次launch变1次
- FlashAttention将多个步骤融合 → 显著减少launch次数
- NVFuser/torch.compile自动做element-wise融合
3. Persistent Kernel(长驻kernel):
1 | __global__ void persistent_kernel(WorkQueue* queue) { |
- Kernel不退出,循环等待新任务
- 适合任务频繁到达的场景(如流式处理)
4. 其他方法:
- 减少不必要的
cudaDeviceSynchronize()(同步会强制等待所有kernel完成) - 使用多Stream并发launch(CPU可以连续提交而不阻塞)
- 增大单个kernel的工作量(少量大kernel优于大量小kernel)
Q: CUDA编程中Bank Conflict是什么?怎么解决?
什么是Bank Conflict?
GPU共享内存被组织为32个bank,每个bank宽4字节。连续的4字节地址映射到连续的bank:
1 | 地址 0-3 → Bank 0 |
当同一warp中的多个线程同时访问同一bank的不同地址时,这些访问被串行化(N-way conflict → N倍延迟)。
典型冲突示例:
1 | __shared__ float s[32][32]; |
解决方案:
方法1: Padding(最简单有效):
1 | __shared__ float s[32][33]; // 多加1列 |
代价:浪费少量共享内存(32×4=128字节/矩阵)
方法2: Swizzle(地址重映射):
1 | // 通过XOR运算重映射行列索引,使访问模式无冲突 |
无额外内存开销,但代码复杂度增加。Cutlass库大量使用此技术。
方法3: 向量化访问:
1 | // 使用float4一次读4个连续元素,跨越4个bank |
判断是否有bank conflict: Nsight Compute中查看Shared Memory Bank Conflicts指标。
Q: 场景题:大集群中节点内有NVLink,节点间部分机器有RDMA,如何设计分布式推理方案?
设计思路——感知网络拓扑,匹配通信需求与互联能力:
核心原则: 通信密集的并行方式用高带宽互联,通信稀疏的用低带宽互联。
1 | ┌─── Node 1 (NVLink互联) ───┐ ┌─── Node 2 (NVLink互联) ───┐ |
具体方案:
| 层级 | 并行方式 | 互联 | 原因 |
|---|---|---|---|
| 节点内(8卡) | 张量并行(TP=8) | NVLink(600GB/s) | TP每层需要AllReduce,通信频繁且数据量大 |
| RDMA节点间 | 流水线并行(PP) | RDMA(~50GB/s) | PP只传激活值(batch_size×hidden_size),通信量小 |
| 无RDMA节点 | 数据并行/不参与 | TCP | 不适合参与同一推理任务 |
详细设计考量:
TP组划分:同一TP组必须在NVLink互联的GPU上
- A100 NVLink带宽600GB/s,AllReduce一个14GB的hidden state只需~25ms
- 如果用PCIe(32GB/s)做TP,延迟增加20倍,不可接受
PP调度:使用RDMA连接的节点组成PP pipeline
- 每次传递的数据量 = micro_batch_size × seq_len × hidden_size × 2bytes(FP16)
- 典型值:1×2048×4096×2 = 16MB,RDMA传输约0.3ms,可接受
容错设计:
- RDMA链路故障:将该节点的PP stage迁移到备用节点
- 单GPU故障:该TP组整体不可用,需要reconfigure
调度器实现:
- 维护集群拓扑图(NVLink/RDMA/TCP连接关系)
- 请求路由时做亲和性调度:同一推理任务的TP组绑定到NVLink节点
- 监控链路健康状态,动态调整路由
Q: 手撕:K个一组翻转链表?
(编程题)