1000道算法工程师面试题(大模型)—— 第15部分
摘要:本文针对深度学习与分布式系统中的典型故障场景,提供了一套实战排查方法论。核心问题包括:推理超时(GIL/GC阻塞)、训练NaN(学习率/数据异常)、多卡死锁(NCCL通信)、显存泄漏(计算图残留)、I/O性能瓶颈等。每个问题均给出分层诊断工具链(如py-spy、NCCL_DEBUG、torch内存分析)和修复方案,强调从监控指标(GPU利用率、网络拓扑)到代码细节(同步I/O、梯度裁剪)的系
·
这是一个关于 实战编码能力与故障排查(Troubleshooting) 的深度面试题集。这一部分往往是“面霸”和“实干家”的分水岭,因为它需要真实的踩坑经验。
201. 线上推理接口偶尔超时,但 CPU/GPU、网络监控都很正常,你会如何一步步排查问题?
核心思路:资源未饱和但响应慢,通常是“锁竞争”或“同步阻塞”。
- 排查 Python GIL (Global Interpreter Lock):
- 虽然整体 CPU 利用率不高(如 48 核只用了 5%),但可能主线程(处理 HTTP 请求的线程)被跑满了。
- 工具: 使用
py-spy top --pid <pid>实时查看线程状态,看是否都在等待 GIL。
- 排查 GC (Garbage Collection) 停顿:
- Python 的循环引用垃圾回收会暂停所有线程(Stop-the-world)。如果对象创建销毁极频繁,GC 可能耗时几百毫秒。
- 动作: 开启 GC 日志
gc.set_debug(gc.DEBUG_STATS)或在代码中打印gc.get_stats()。
- 排查同步 I/O 阻塞:
- 是否在异步框架(FastAPI/Tornado)里混用了同步代码?比如直接用了
requests.get或同步写日志。 - 工具: 检查代码,或用
strace -p <pid>查看是否有长时间的write/read系统调用。
- 是否在异步框架(FastAPI/Tornado)里混用了同步代码?比如直接用了
- 排查线程池/连接池耗尽:
- 如果依赖 Redis/MySQL,连接池满了,请求会在队列里排队,此时 CPU/GPU 都不干活,纯等。
- 动作: 检查应用层 metrics(如
active_connections)。
202. 给你一段 PyTorch 训练代码,Loss 一直是 NaN,你会从哪些方面依次检查?
排查 Checklist(按概率从大到小):
- 学习率 (Learning Rate): 太大了。尝试降低 10 倍甚至 100 倍。
- 数据脏值: 输入数据中是否包含
NaN或Inf?(例如图片像素除以 0,文本 Embedding 越界)。- 检测:
assert not torch.isnan(input).any()。
- 检测:
- 混合精度溢出 (FP16 Overflow): 如果用了 FP16,数值范围很小,容易溢出。
- 解决: 使用
bfloat16(如果硬件支持)或检查GradScaler是否正常工作(loss scale 是否无限缩小)。
- 解决: 使用
- 算子数值稳定性:
- 除零风险:
x / (y + epsilon),epsilon 设得太小(如 1e-12 在 fp16 下等于 0)。 - Log(<=0):
torch.log(prob),如果 prob 是 0,结果是-inf。
- 除零风险:
- 梯度爆炸:
- 检测:
torch.nn.utils.clip_grad_norm_是否生效?打印梯度 norm 值。
- 检测:
- 终极工具: 在代码开头加
torch.autograd.set_detect_anomaly(True),它会回溯并报错在产生 NaN 的那一行算子(会严重拖慢速度,仅调试用)。
203. 假设一个多卡训练任务经常在某个 step 卡死不动,你会如何判断是通信问题、数据问题还是算子问题?
定位步骤:
- 看日志 (Rank 0 vs Others):
- 是所有卡都卡住了,还是只有 Rank 0 在跑,其他卡在等?
- 如果是 Rank 0 在打印日志,其他卡没动静,可能是数据加载不均匀,Rank 0 有数据,Rank N 数据读完了在空等 Barrier。
- 看 GPU 状态 (
nvidia-smi):- GPU 利用率 100%: 说明卡在计算算子(死循环或极慢的 Kernel)。
- GPU 利用率 0%: 说明卡在 CPU 处理或 NCCL 通信。
- NCCL Debug:
- 设置环境变量
NCCL_DEBUG=INFO。 - 如果日志停在
NCCL WARN: Waiting for ncclCommInitRank或类似 P2P 通信处,说明是物理链路或防火墙问题。
- 设置环境变量
- 堆栈分析 (
py-spy/gdb):- 直接 attach 到进程:
py-spy dump --pid <pid>。 - 如果调用栈停在
tensor.to(device)或dataloader,是数据问题。 - 如果停在
dist.all_reduce,是通信死锁。
- 直接 attach 到进程:
204. 模型推理过程中显存会“缓慢上涨直到 OOM”,你会用哪些工具和方法来定位泄漏点?
常见原因与工具:
- 计算图残留 (Common Mistake):
- 代码模式:
loss_list.append(loss)。这会把整个计算图存下来。 - 修正:
loss_list.append(loss.item())或loss.detach()。
- 代码模式:
- 缓存未清理:
- 检查是否使用了全局的
dict或list缓存了中间结果(如 kv-cache)且没设置最大长度。
- 检查是否使用了全局的
- 工具定位:
torch.cuda.memory_summary(): 打印 PyTorch 显存分配详情,看是Allocated(实际占用) 涨还是Reserved(缓存) 涨。tracemalloc(Python): 监控 Python 对象增长。objgraph: 生成引用关系图,看哪个对象(如 Tensor)数量在疯涨。
205. 有一个数据处理脚本单机跑要 10 小时,你会采取哪些策略做“工程级加速”?
- Profiling 第一: 不要盲目优化。用
cProfile或line_profiler找出哪一行代码慢。- 如果是 I/O 慢(读图片/下文件): 改多线程 (
ThreadPoolExecutor) 或 AsyncIO。 - 如果是计算慢(正则/Tokenize): 改多进程 (
ProcessPoolExecutor)。
- 如果是 I/O 慢(读图片/下文件): 改多线程 (
- 向量化 (Vectorization):
- 把 Python 的
for循环改成 Pandas 或 NumPy 的矩阵操作。速度通常提升 10-100 倍。
- 把 Python 的
- 分布式处理:
- 如果单机实在搞不定,用 Ray Data 或 Spark。把脚本改造成 Map-Reduce 模式。
- 序列化格式:
- 别用 CSV/JSON 读写中间结果,慢且占空间。改用 Parquet 或 Arrow (Zero-copy)。
- JIT 编译:
- 对于纯数学计算密集型函数,加个
@numba.jit装饰器。
- 对于纯数学计算密集型函数,加个
206. 在 Linux 上,如何快速定位“是谁在疯狂写盘/读盘”,以及这对训练/推理有什么影响?
定位工具:
iotop -oP:实时显示正在进行磁盘 I/O 的进程。pidstat -d 1:查看具体 PID 的读写速率。lsof -p <PID>:查看该进程具体打开了哪些文件(是在写日志?还是在存 Checkpoint?)。
影响:
- 训练: 严重的磁盘 I/O 争抢会导致 DataLoader 读取变慢,GPU 因为等数据而空转(Utilization 下降),拖慢训练时长。
- 推理: 如果日志级别没设好(Debug),大量同步写日志会阻塞主线程,导致 API 延迟抖动(Latency Spike)。
207. 线上服务偶发 500,但日志很多很乱,你会如何设计一套日志规范和排错流程?
设计规范:
- TraceID 全链路追踪:
- 在网关层生成唯一的
RequestID,透传给后端所有微服务。所有日志必须带上[RequestID: xxxx]。 - 这样 grep 一下 ID 就能把一次请求的所有日志串起来。
- 在网关层生成唯一的
- 结构化日志 (JSON Logging):
- 不要打印
print("Error happened")。 - 要打印
{"level": "ERROR", "timestamp": "...", "msg": "Error...", "trace_id": "...", "user_id": "..."}。便于 ELK/Loki 索引分析。
- 不要打印
- 上下文保留:
- 在报错
except块中,不仅打印堆栈,还要打印当前函数的入参(注意脱敏),否则很难复现。
- 在报错
- 分级策略:
- 平时只开
INFO。报错时由 Sentry 或类似工具捕获完整的 Traceback 和环境快照。
- 平时只开
208. 分布式训练中,有一台机器经常报 NCCL timeout,你会有哪些具体的排查动作?
物理与配置层排查:
- 硬件检查:
dmesg | grep -i xid:查看是否有 GPU 掉卡或 PCIe 错误。ibstat:检查 InfiniBand 网卡状态,是否有 LinkDown 或 SymbolError。- 热节流: 检查该机器 GPU 温度是否过高导致降频,拖慢了整体通信节奏。
- 网络拓扑:
nvidia-smi topo -m:检查该机器的拓扑是否与其他机器一致。
- NCCL 环境变量:
- 尝试设置
NCCL_IB_DISABLE=1强制走以太网,如果恢复正常,说明 IB 网络有问题。 - 尝试设置
NCCL_P2P_DISABLE=1,如果恢复,说明 PCIe P2P 传输有问题。
- 尝试设置
- Benchmarks:
- 运行
all_reduce_perf基准测试,点对点测试该机器与其他机器的带宽,找出“慢节点”。
- 运行
209. 一段 Python 多线程代码速度反而比单线程更慢,你会从哪几个点分析原因?
核心原因:GIL (Global Interpreter Lock)。
分析:
- 任务类型:
- 如果是 CPU 密集型(如矩阵乘法、正则匹配、图像处理),Python 多线程不仅无法利用多核,反而因为频繁竞争 GIL 和上下文切换(Context Switch)增加了开销,导致变慢。
- 解决: 改用
multiprocessing或 C++ 扩展。
- 任务粒度:
- 如果任务极小(如
a+b),创建线程的开销远大于执行任务的开销。
- 如果任务极小(如
- 例外:
- 如果是 I/O 密集型(爬虫、请求 API),多线程是有效的,因为 I/O 等待时会释放 GIL。
210. 给你一个“间歇性失败”的接口(偶尔请求成功、偶尔报错),你会如何重现实验和收集信息?
复现与收集策略:
- 流量录制与回放:
- 在网关层开启“完整报文日志”,记录失败请求的 Header、Body。
- 在本地用 Postman 或脚本尝试重放该请求,看是否必现。
- 压力测试/并发测试:
- 如果单次不复现,可能是并发竞争条件 (Race Condition)。
- 写脚本并发请求接口,观察错误率。
- 状态依赖分析:
- 检查报错是否与特定数据有关(如某个特殊字符、超长文本)。
- 检查是否与特定 Pod有关(负载均衡到了坏节点)。
- 依赖服务检查:
- 是不是下游数据库或 Redis 偶尔超时?(查看下游监控的 P99 和 Error Rate)。
211. 有一个训练任务本地能复现线上问题,但线上环境不能改代码,你会如何做“远程调试”?
无侵入调试手段:
- 环境变量控制日志:
- 大多数框架支持通过 env 调整日志级别。如
LOG_LEVEL=DEBUG或NCCL_DEBUG=INFO,重启进程即可生效,无需改代码。
- 大多数框架支持通过 env 调整日志级别。如
- 系统级追踪 (strace/tcpdump):
strace -p <pid>:查看进程在做什么系统调用(卡在文件锁?卡在网络读取?)。tcpdump:抓包看网络交互数据。
- 非侵入式 Profiler (py-spy):
py-spy dump --pid <pid>:可以直接打印出 Python 进程当前的调用栈,不用停止服务,不用改代码。这是排查死锁和卡顿的神器。
- 动态注入 (GDB - 极高风险):
- 用 GDB attach 到 Python 解释器,手动执行
PyRun_SimpleString来打印变量。仅限死马当活马医。
- 用 GDB attach 到 Python 解释器,手动执行
212. 监控发现 GPU 利用率只有 20%,而 CPU 和网络都不高,你会如何排查瓶颈在哪里?
排查“Kernel Launch Bound”与代码逻辑:
- Kernel 太碎:
- 如果模型由大量极小的算子(如 slice, add, view)组成,CPU 发射算子的速度赶不上 GPU 执行的速度。
- 解决: 使用
CUDA Graphs或torch.compile进行算子融合。
- Python 逻辑阻塞:
- 在 GPU 计算之间,夹杂了繁重的 Python
for循环或 CPU 标量运算。 - 现象:
nvidia-smi看起来在闪烁(一下 100% 一下 0%)。
- 在 GPU 计算之间,夹杂了繁重的 Python
- 数据传输 (H2D/D2H):
- 频繁使用
.item()或.cpu()将数据从 GPU 拉回 CPU 打印或判断逻辑,导致同步阻塞。
- 频繁使用
- 磁盘 IO 小文件:
- 虽然 CPU 不高,但可能处于
iowait状态(读取大量小文件)。检查top命令的wa指标。
- 虽然 CPU 不高,但可能处于
213. Docker 容器内的程序和宿主机行为不一致(如文件访问、时区、网络),你会如何排查?
- 文件/权限:
- Mount: 检查
docker run -v挂载路径是否正确。 - User ID: 宿主机是
root(uid 0),容器内可能是appuser(uid 1000)。容器内没权限读写挂载卷。
- Mount: 检查
- 时区 (Timezone):
- Docker 默认是 UTC。宿主机是 CST。
- 排查:
date命令。解决: 挂载/etc/localtime或设置TZ环境变量。
- 网络隔离:
- DNS: 容器内的
/etc/resolv.conf可能和宿主机不同,导致解析不到内网域名。 - Localhost: 容器内的
localhost是容器自己,不是宿主机。连接宿主机服务需用host.docker.internal或--network host。
- DNS: 容器内的
- 资源限制 (Cgroups):
- 宿主机内存很大,但容器被 k8s 限制了 Memory Limit,导致程序被 OOM Killer 杀掉(宿主机没挂,容器挂了)。
214. 一段使用多进程 DataLoader 的 PyTorch 代码,在 Windows 和 Linux 上行为不同,你会怎么定位?
核心差异:进程启动方式 (Spawn vs Fork)。
- Linux (
fork):- 子进程复制父进程的内存空间(Copy-on-Write),速度快,且能共享父进程的全局变量/Socket。
- Windows (
spawn):- 子进程会重新导入一遍脚本,重新初始化所有全局变量。
- 典型报错:
BrokenPipeError或程序无限递归启动。 - 解决: Windows 上必须把启动代码放在
if __name__ == '__main__':之下。 - 数据问题: 在
spawn模式下,传递给 DataLoader 的 dataset 对象必须是可序列化(Picklable)的,lambda 函数或某些闭包会报错。
215. 当你接手一段别人写的、缺乏注释的大型训练/推理代码,你会用什么方法快速“吃透”和重构?
三步走策略:Run it -> Trace it -> Refactor it。
- Run it (环境复现):
- 先别看代码,先看能不能跑起来。配置 Docker/Conda 环境,解决依赖报错。
- 跑通一个最小的 Demo(如
batch_size=1,max_steps=10),确保管线是通的。
- Trace it (动态分析):
- 加日志/断点: 在关键入口(如
forward,data_process)打印 tensor 的 shape。 - IDE 调试: 使用 VSCode/PyCharm 的 Debug 模式,一步步跟进(Step Into),看数据流怎么变。这是理解代码逻辑最快的方法,比干读快 10 倍。
- 生成类图: 使用
pyreverse等工具生成类关系图,理清架构。
- 加日志/断点: 在关键入口(如
- Refactor it (测试驱动重构):
- 在修改前,先写一个金标准测试 (Golden Test):固定输入,记录下现在的输出。
- 重构时,每改动一点,就跑一遍测试,确保输出和原来一致,防止改坏。
更多推荐

所有评论(0)