卓驭 AI Infra 校招 一面
Q: 算子优化从Profiling到落地的完整流程?如何判断一个算子是memory-bound还是compute-bound?
完整流程(工业实践中的闭环):
1 | 1. Profiling(定位热点) |
判断Bound类型——Roofline Model:
1 | Performance (FLOPS) |
具体计算方法:
以A100为例:
- Peak FP16 FLOPS = 312 TFLOPS
- Peak HBM BW = 2 TB/s
- 拐点 = 312T / 2T = 156 FLOPs/Byte
对于给定kernel:
1 | # GEMM [M, K] × [K, N] 的算术强度估算 |
Nsight Compute直接判断:
Memory Throughput/Peak Memory BW> 70%且Compute Throughput/Peak Compute低 → Memory-bound。- 反之 → Compute-bound。
- Nsight Compute的
Speed of Light面板直接显示两者的达峰百分比。
Q: 用过哪些Profiling工具?Nsight Systems能看到指令级流水吗?
工具层次对比:
| 工具 | 粒度 | 功能 | 适用阶段 |
|---|---|---|---|
| Nsight Systems | 系统级 | 时间线、kernel/API调用序列 | 定位热点 |
| Nsight Compute | Kernel级 | 详细性能计数器、Roofline | 优化单kernel |
| cuPTI | API级 | 可编程性能采集 | 自建监控 |
| nvprof/nvvp | 已弃用 | 旧版profile | 兼容旧环境 |
| torch.profiler | 框架级 | Op级耗时+内存 | Python层快速定位 |
Nsight Systems能做什么:
- CPU和GPU的联合时间线(看到Python调用→CUDA API→kernel执行的完整链路)。
- 多GPU时间线对齐(看通信和计算的overlap)。
- NVTX标注区域的耗时统计。
- API调用频率和开销(如cudaMalloc/cudaLaunchKernel的频率)。
Nsight Systems不能做什么:
- 不能看指令级流水(每条指令的issue/execute/retire时序)。
- 不能看单个warp的执行历史。
- 不能看寄存器分配和spill详情。
指令级分析需要什么:
- Nsight Compute的Source页面:可以看每条SASS指令的采样hit count(类似perf的热点行)。这是统计近似,不是精确的逐指令时序。
- Nsight Compute的Warp State统计:按stall原因分类(memory dependency/barrier/instruction fetch等),给出warp时间分布。
- SASS级汇编分析:cuobjdump反汇编kernel的SASS代码,人工分析指令依赖链和吞吐瓶颈。
- 硬件模拟器(GPGPU-Sim等):学术工具,可模拟逐cycle的流水线行为,但与实际硬件可能有偏差。
实践中的Profiling工作流:
1 | 第一步: nsys看全局 → 发现matmul_kernel占总时间40% |
Q: Warp利用率低怎么归因?负载不均衡怎么解决?
Warp利用率低的归因分析:
在Nsight Compute中查看Warp State Statistics面板,各stall原因对应不同的root cause:
| Stall原因 | 含义 | 典型root cause | 解决方向 |
|---|---|---|---|
| Long Scoreboard | 等待全局内存/L2返回 | 数据预取不足、cache miss | prefetch、增大tiling |
| Barrier | 等待__syncthreads() | Block内warp进度不一致 | 减少sync次数、减小block |
| Not Selected | 就绪但未被调度 | 正常(其他warp在执行) | 增加ILP |
| Wait | 等待固定延迟指令完成 | 计算依赖链过长 | 循环展开、增加独立指令 |
| Stall MIO Throttle | shared memory bank conflict | 多线程同时访问同bank | padding、改访问模式 |
| Dispatch Stall | 指令发射受限 | 指令cache miss或调度器满 | 简化kernel逻辑 |
利用率低的常见原因:
Block尾部不满warp:block大小非32倍数,最后一个warp只有部分线程活跃。
1
Block=100 → warp 0(32线程), warp 1(32线程), warp 2(32线程), warp 3(仅4线程活跃!)
解决:block大小始终设为32的倍数。
分支Divergence:同一warp内线程走不同分支,串行执行两条路径。
1
2if (tid % 2 == 0) heavy_work(); // 50% divergence!
else light_work();解决:数据重排使同warp线程走相同路径,或用predication替代分支。
数据依赖导致活跃warp不足:每线程使用大量寄存器→SM能驻留的warp少→延迟隐藏不足。
解决:减少寄存器用量(__launch_bounds__限制)或使用double buffering流水化。
负载不均衡的解决方案:
1. 动态任务分配(Work Stealing):
1 | __device__ int global_counter = 0; // 全局任务计数器 |
每个线程做完当前任务后从全局计数器取下一个,自动均衡。代价:原子操作开销(但任务粒度够大时可忽略)。
2. Grid-stride Loop(最简单有效):
1 | __global__ void kernel(float* data, int N) { |
每线程处理多个元素,天然均衡(除非process耗时不均)。
3. Persistent Kernel(极致负载均衡):
1 | __global__ void persistent_kernel(TaskQueue* queue) { |
适合任务粒度差异大的场景(如稀疏矩阵、图算法)。
4. 数据重排/Padding:
1 | // 不均衡:不同行长度不同(如CSR稀疏矩阵的各行非零元数差异大) |
Q: 昇腾NPU和NVIDIA GPU架构差异?内存层级设计?
核心架构对比:
| 维度 | NVIDIA GPU (A100) | 华为昇腾NPU (910B) |
|---|---|---|
| 计算核心 | CUDA Core(通用) + Tensor Core(矩阵) | Cube(矩阵) + Vector(向量) + Scalar(标量) |
| 计算粒度 | Warp(32线程SIMT) | 向量运算(2048-bit一次处理128个FP16) |
| 矩阵运算 | Tensor Core: 4×4 FMA/cycle | Cube: 16×16 FMA/cycle |
| 编程模型 | CUDA (相对隐式内存管理) | Ascend C (显式数据搬运) |
| Cache策略 | L1/L2 Cache硬件自动管理 | 无自动Cache,靠程序员显式管理Buffer |
| 内存层级数 | 3级(Registers→L1/Shared→L2→HBM) | 4级(L0→L1 Buffer→L2→HBM) |
| 总带宽(HBM) | 2 TB/s | ~1.6 TB/s |
| 峰值算力(FP16) | 312 TFLOPS | ~320 TFLOPS |
内存层级详细对比:
1 | NVIDIA GPU: 华为昇腾NPU: |
编程模型的核心差异:
GPU(CUDA):
1 | // 程序员无需显式搬运数据,硬件Cache自动管理 |
NPU(Ascend C):
1 | // 必须显式管理数据搬运,否则每次计算都从HBM读取(极慢) |
关键差异总结:
- GPU有Cache兜底:即使不优化shared memory,程序也能正确运行(只是慢)。
- NPU无Cache兜底:不手动搬运=每次从HBM读=极慢。编程者必须理解内存层次。
- 这使得NPU编程更接近传统DSP/FPGA的思维模式,对程序员要求更高。
- NPU优势:显式管理带来更可预测的性能,适合固定计算模式的深度学习算子。
Q: 多进程和多线程的性能区别?
系统级对比:
| 维度 | 多线程 | 多进程 |
|---|---|---|
| 地址空间 | 共享(同一虚拟地址空间) | 独立(各自的地址空间) |
| 创建开销 | ~10us (clone少量数据结构) | ~1ms (fork/exec,复制页表) |
| 上下文切换 | ~1-5us (仅保存寄存器) | ~10-100us (切换页表+刷TLB) |
| 通信方式 | 直接读写共享变量 | IPC(管道/shm/socket) |
| 同步 | mutex/condvar/atomic | 信号量/消息/文件锁 |
| 故障隔离 | 无(一线程crash全进程挂) | 有(一进程crash不影响其他) |
| 内存开销 | 仅栈空间(~8MB/thread) | 完整进程空间(虚拟内存COW) |
性能关键差异:
1. 数据共享效率:
1 | # 多线程:直接共享内存,零拷贝 |
多线程共享大数据(如模型权重、特征图)时性能远超多进程(后者需要显式共享内存映射或数据序列化)。
2. Python GIL的影响:
1 | # Python多线程受GIL限制,无法真正并行CPU计算 |
3. GPU训练中的选择:
DDP使用多进程而非多线程的原因:
- 绕过Python GIL(每个进程独立Python解释器)。
- 每进程独立管理一块GPU的CUDA context(CUDA context不能跨线程安全共享所有操作)。
- 进程隔离:一个进程OOM/crash不影响其他(可以重启恢复)。
- 内存隔离:每进程独立的显存空间,NCCL通信明确定义。
4. 数据加载的选择:
1 | # PyTorch DataLoader使用多进程 |
性能选择决策树:
1 | 任务需要共享大量数据且IO密集? → 多线程 |
Q: KV Cache、算子融合、量化分别如何优化推理?
三种技术从不同层面加速推理,且可叠加使用:
KV Cache(消除冗余计算):
1 | 无KV Cache (每步计算全序列attention): |
- 效果:decode从O(n^2)总计算降为O(n)每步(累计仍O(n^2)但节省了n倍重复计算)。
- 代价:需要O(n×layers×kv_heads×head_dim)的显存存储KV Cache。
- 进一步优化:PagedAttention(内存管理)→GQA(减少KV头数)→量化KV(减少每token字节)。
数值示例(LLaMA-7B, seq=2K, FP16):
- 无KV Cache每步计算量:~2K × 2K × 4096 × 32层 × 2 ≈ 34T FLOPs(不现实!)
- 有KV Cache每步计算量:1 × 2K × 4096 × 32层 × 2 ≈ 17G FLOPs(合理)
算子融合(减少内存带宽浪费):
1 | 未融合(3个独立kernel): |
对于memory-bound操作(大多数非GEMM操作),I/O减少直接等比加速。
典型融合收益:
- Add+LayerNorm融合:2x加速。
- QKV三个GEMM合一:1.3x加速(减少kernel launch + 输入只读一次)。
- GEMM+bias+activation: 1.1x加速(bias和activation几乎零开销融合到epilogue)。
量化(减少数据传输量):
1 | FP16权重 (70B模型): 140GB |
Decode阶段是memory-bound(AI≈2),权重读取是瓶颈。量化直接减少需要读取的字节数:
| 精度 | 模型大小(70B) | 理论decode加速(vs FP16) | 实际加速 |
|---|---|---|---|
| FP16 | 140GB | 1x | baseline |
| INT8 | 70GB | 2x | 1.7-1.9x |
| INT4 | 35GB | 4x | 2.5-3x |
| FP8 | 70GB | 2x | 1.8x |
实际加速低于理论值因为:反量化计算开销、kernel效率、量化不能加速attention中的KV Cache读取。
三者叠加效果:
- KV Cache(基础必备)+ 算子融合(全链路优化)+ W4A16量化:总计可达5-10x加速(相比naive实现)。
Q: 模型输出与预期不符怎么debug?误差累积怎么解决?
系统化Debug方法:
Step 1: 确认问题范围
1 | # 首先检查是全部输出错还是部分 |
Step 2: 逐层二分定位
1 | # 在模型中间插入hook,逐层对比 |
Step 3: 单层深入分析
- 确认权重完全一致(bit-exact对比加载的权重)。
- 确认输入完全一致(包括attention mask、position ids)。
- 检查数据类型是否匹配(FP32 vs FP16可能在softmax等处产生差异)。
- 检查是否有NaN/Inf(
torch.isnan(output).any())。
Step 4: 常见bug类型:
| 症状 | 可能原因 | 验证方法 |
|---|---|---|
| 输出全为NaN | 除以0/exp溢出 | 在softmax前检查score范围 |
| 输出全为0 | ReLU过度kill/权重错位 | 检查激活分布 |
| 输出正确但偏移 | bias遗漏/scale错误 | 检查各层的bias和scale |
| 精度逐层下降 | 累积误差/精度不够 | 逐层比较误差增长曲线 |
| 随机错误 | 未初始化内存/竞态 | 固定seed+deterministic模式 |
误差累积的解决方案:
问题来源:每层的微小误差(如量化舍入、低精度计算)经过N层传播后可能放大到不可接受。
1 | Layer 0: err = 1e-4 |
解决策略:
1. 关键层用高精度:
1 | # Softmax和LayerNorm对精度最敏感(涉及exp和除法) |
2. 混合精度累加:
- GEMM:输入FP16/INT8,累加器FP32/INT32。Tensor Core天然支持。
- Reduce操作(sum/mean):始终用FP32累加。
- Softmax的exp和sum:FP32计算后转回FP16。
3. 量化感知训练(QAT):
1 | # 训练时模拟量化误差,让模型适应 |
4. 逐层校准优化:
- 使用更精细的量化粒度(per-group而非per-tensor)。
- 选择更好的校准方法(percentile/KL散度而非minmax)。
- 对误差敏感层使用更高精度(如attention层FP16,其他INT8)。
5. 输出校正:
- 如果已知系统性偏移(如量化导致的均值偏移),可以加learnable correction layer。
- AdaRound等方法在量化时考虑层间误差传播进行联合优化。