这是一个关于 实战编码能力与故障排查(Troubleshooting) 的深度面试题集。这一部分往往是“面霸”和“实干家”的分水岭,因为它需要真实的踩坑经验。


201. 线上推理接口偶尔超时,但 CPU/GPU、网络监控都很正常,你会如何一步步排查问题?

核心思路:资源未饱和但响应慢,通常是“锁竞争”或“同步阻塞”。

  1. 排查 Python GIL (Global Interpreter Lock):
    • 虽然整体 CPU 利用率不高(如 48 核只用了 5%),但可能主线程(处理 HTTP 请求的线程)被跑满了。
    • 工具: 使用 py-spy top --pid <pid> 实时查看线程状态,看是否都在等待 GIL。
  2. 排查 GC (Garbage Collection) 停顿:
    • Python 的循环引用垃圾回收会暂停所有线程(Stop-the-world)。如果对象创建销毁极频繁,GC 可能耗时几百毫秒。
    • 动作: 开启 GC 日志 gc.set_debug(gc.DEBUG_STATS) 或在代码中打印 gc.get_stats()
  3. 排查同步 I/O 阻塞:
    • 是否在异步框架(FastAPI/Tornado)里混用了同步代码?比如直接用了 requests.get 或同步写日志。
    • 工具: 检查代码,或用 strace -p <pid> 查看是否有长时间的 write/read 系统调用。
  4. 排查线程池/连接池耗尽:
    • 如果依赖 Redis/MySQL,连接池满了,请求会在队列里排队,此时 CPU/GPU 都不干活,纯等。
    • 动作: 检查应用层 metrics(如 active_connections)。

202. 给你一段 PyTorch 训练代码,Loss 一直是 NaN,你会从哪些方面依次检查?

排查 Checklist(按概率从大到小):

  1. 学习率 (Learning Rate): 太大了。尝试降低 10 倍甚至 100 倍。
  2. 数据脏值: 输入数据中是否包含 NaNInf?(例如图片像素除以 0,文本 Embedding 越界)。
    • 检测: assert not torch.isnan(input).any()
  3. 混合精度溢出 (FP16 Overflow): 如果用了 FP16,数值范围很小,容易溢出。
    • 解决: 使用 bfloat16(如果硬件支持)或检查 GradScaler 是否正常工作(loss scale 是否无限缩小)。
  4. 算子数值稳定性:
    • 除零风险: x / (y + epsilon),epsilon 设得太小(如 1e-12 在 fp16 下等于 0)。
    • Log(<=0): torch.log(prob),如果 prob 是 0,结果是 -inf
  5. 梯度爆炸:
    • 检测: torch.nn.utils.clip_grad_norm_ 是否生效?打印梯度 norm 值。
  6. 终极工具: 在代码开头加 torch.autograd.set_detect_anomaly(True),它会回溯并报错在产生 NaN 的那一行算子(会严重拖慢速度,仅调试用)。

203. 假设一个多卡训练任务经常在某个 step 卡死不动,你会如何判断是通信问题、数据问题还是算子问题?

定位步骤:

  1. 看日志 (Rank 0 vs Others):
    • 是所有卡都卡住了,还是只有 Rank 0 在跑,其他卡在等?
    • 如果是 Rank 0 在打印日志,其他卡没动静,可能是数据加载不均匀,Rank 0 有数据,Rank N 数据读完了在空等 Barrier。
  2. 看 GPU 状态 (nvidia-smi):
    • GPU 利用率 100%: 说明卡在计算算子(死循环或极慢的 Kernel)。
    • GPU 利用率 0%: 说明卡在 CPU 处理或 NCCL 通信。
  3. NCCL Debug:
    • 设置环境变量 NCCL_DEBUG=INFO
    • 如果日志停在 NCCL WARN: Waiting for ncclCommInitRank 或类似 P2P 通信处,说明是物理链路或防火墙问题。
  4. 堆栈分析 (py-spy / gdb):
    • 直接 attach 到进程:py-spy dump --pid <pid>
    • 如果调用栈停在 tensor.to(device)dataloader,是数据问题。
    • 如果停在 dist.all_reduce,是通信死锁。

204. 模型推理过程中显存会“缓慢上涨直到 OOM”,你会用哪些工具和方法来定位泄漏点?

常见原因与工具:

  1. 计算图残留 (Common Mistake):
    • 代码模式: loss_list.append(loss)。这会把整个计算图存下来。
    • 修正: loss_list.append(loss.item())loss.detach()
  2. 缓存未清理:
    • 检查是否使用了全局的 dictlist 缓存了中间结果(如 kv-cache)且没设置最大长度。
  3. 工具定位:
    • torch.cuda.memory_summary() 打印 PyTorch 显存分配详情,看是 Allocated (实际占用) 涨还是 Reserved (缓存) 涨。
    • tracemalloc (Python): 监控 Python 对象增长。
    • objgraph 生成引用关系图,看哪个对象(如 Tensor)数量在疯涨。

205. 有一个数据处理脚本单机跑要 10 小时,你会采取哪些策略做“工程级加速”?

  1. Profiling 第一: 不要盲目优化。用 cProfileline_profiler 找出哪一行代码慢。
    • 如果是 I/O 慢(读图片/下文件): 改多线程 (ThreadPoolExecutor) 或 AsyncIO。
    • 如果是计算慢(正则/Tokenize): 改多进程 (ProcessPoolExecutor)。
  2. 向量化 (Vectorization):
    • 把 Python 的 for 循环改成 Pandas 或 NumPy 的矩阵操作。速度通常提升 10-100 倍。
  3. 分布式处理:
    • 如果单机实在搞不定,用 Ray DataSpark。把脚本改造成 Map-Reduce 模式。
  4. 序列化格式:
    • 别用 CSV/JSON 读写中间结果,慢且占空间。改用 ParquetArrow (Zero-copy)。
  5. JIT 编译:
    • 对于纯数学计算密集型函数,加个 @numba.jit 装饰器。

206. 在 Linux 上,如何快速定位“是谁在疯狂写盘/读盘”,以及这对训练/推理有什么影响?

定位工具:

  1. iotop -oP:实时显示正在进行磁盘 I/O 的进程。
  2. pidstat -d 1:查看具体 PID 的读写速率。
  3. lsof -p <PID>:查看该进程具体打开了哪些文件(是在写日志?还是在存 Checkpoint?)。

影响:

  • 训练: 严重的磁盘 I/O 争抢会导致 DataLoader 读取变慢,GPU 因为等数据而空转(Utilization 下降),拖慢训练时长。
  • 推理: 如果日志级别没设好(Debug),大量同步写日志会阻塞主线程,导致 API 延迟抖动(Latency Spike)。

207. 线上服务偶发 500,但日志很多很乱,你会如何设计一套日志规范和排错流程?

设计规范:

  1. TraceID 全链路追踪:
    • 在网关层生成唯一的 RequestID,透传给后端所有微服务。所有日志必须带上 [RequestID: xxxx]
    • 这样 grep 一下 ID 就能把一次请求的所有日志串起来。
  2. 结构化日志 (JSON Logging):
    • 不要打印 print("Error happened")
    • 要打印 {"level": "ERROR", "timestamp": "...", "msg": "Error...", "trace_id": "...", "user_id": "..."}。便于 ELK/Loki 索引分析。
  3. 上下文保留:
    • 在报错 except 块中,不仅打印堆栈,还要打印当前函数的入参(注意脱敏),否则很难复现。
  4. 分级策略:
    • 平时只开 INFO。报错时由 Sentry 或类似工具捕获完整的 Traceback 和环境快照。

208. 分布式训练中,有一台机器经常报 NCCL timeout,你会有哪些具体的排查动作?

物理与配置层排查:

  1. 硬件检查:
    • dmesg | grep -i xid:查看是否有 GPU 掉卡或 PCIe 错误。
    • ibstat:检查 InfiniBand 网卡状态,是否有 LinkDown 或 SymbolError。
    • 热节流: 检查该机器 GPU 温度是否过高导致降频,拖慢了整体通信节奏。
  2. 网络拓扑:
    • nvidia-smi topo -m:检查该机器的拓扑是否与其他机器一致。
  3. NCCL 环境变量:
    • 尝试设置 NCCL_IB_DISABLE=1 强制走以太网,如果恢复正常,说明 IB 网络有问题。
    • 尝试设置 NCCL_P2P_DISABLE=1,如果恢复,说明 PCIe P2P 传输有问题。
  4. Benchmarks:
    • 运行 all_reduce_perf 基准测试,点对点测试该机器与其他机器的带宽,找出“慢节点”。

209. 一段 Python 多线程代码速度反而比单线程更慢,你会从哪几个点分析原因?

核心原因:GIL (Global Interpreter Lock)。

分析:

  1. 任务类型:
    • 如果是 CPU 密集型(如矩阵乘法、正则匹配、图像处理),Python 多线程不仅无法利用多核,反而因为频繁竞争 GIL 和上下文切换(Context Switch)增加了开销,导致变慢。
    • 解决: 改用 multiprocessing 或 C++ 扩展。
  2. 任务粒度:
    • 如果任务极小(如 a+b),创建线程的开销远大于执行任务的开销。
  3. 例外:
    • 如果是 I/O 密集型(爬虫、请求 API),多线程是有效的,因为 I/O 等待时会释放 GIL。

210. 给你一个“间歇性失败”的接口(偶尔请求成功、偶尔报错),你会如何重现实验和收集信息?

复现与收集策略:

  1. 流量录制与回放:
    • 在网关层开启“完整报文日志”,记录失败请求的 Header、Body。
    • 在本地用 Postman 或脚本尝试重放该请求,看是否必现。
  2. 压力测试/并发测试:
    • 如果单次不复现,可能是并发竞争条件 (Race Condition)
    • 写脚本并发请求接口,观察错误率。
  3. 状态依赖分析:
    • 检查报错是否与特定数据有关(如某个特殊字符、超长文本)。
    • 检查是否与特定 Pod有关(负载均衡到了坏节点)。
  4. 依赖服务检查:
    • 是不是下游数据库或 Redis 偶尔超时?(查看下游监控的 P99 和 Error Rate)。

211. 有一个训练任务本地能复现线上问题,但线上环境不能改代码,你会如何做“远程调试”?

无侵入调试手段:

  1. 环境变量控制日志:
    • 大多数框架支持通过 env 调整日志级别。如 LOG_LEVEL=DEBUGNCCL_DEBUG=INFO,重启进程即可生效,无需改代码。
  2. 系统级追踪 (strace/tcpdump):
    • strace -p <pid>:查看进程在做什么系统调用(卡在文件锁?卡在网络读取?)。
    • tcpdump:抓包看网络交互数据。
  3. 非侵入式 Profiler (py-spy):
    • py-spy dump --pid <pid>:可以直接打印出 Python 进程当前的调用栈,不用停止服务,不用改代码。这是排查死锁和卡顿的神器。
  4. 动态注入 (GDB - 极高风险):
    • 用 GDB attach 到 Python 解释器,手动执行 PyRun_SimpleString 来打印变量。仅限死马当活马医。

212. 监控发现 GPU 利用率只有 20%,而 CPU 和网络都不高,你会如何排查瓶颈在哪里?

排查“Kernel Launch Bound”与代码逻辑:

  1. Kernel 太碎:
    • 如果模型由大量极小的算子(如 slice, add, view)组成,CPU 发射算子的速度赶不上 GPU 执行的速度。
    • 解决: 使用 CUDA Graphstorch.compile 进行算子融合。
  2. Python 逻辑阻塞:
    • 在 GPU 计算之间,夹杂了繁重的 Python for 循环或 CPU 标量运算。
    • 现象: nvidia-smi 看起来在闪烁(一下 100% 一下 0%)。
  3. 数据传输 (H2D/D2H):
    • 频繁使用 .item().cpu() 将数据从 GPU 拉回 CPU 打印或判断逻辑,导致同步阻塞。
  4. 磁盘 IO 小文件:
    • 虽然 CPU 不高,但可能处于 iowait 状态(读取大量小文件)。检查 top 命令的 wa 指标。

213. Docker 容器内的程序和宿主机行为不一致(如文件访问、时区、网络),你会如何排查?

  1. 文件/权限:
    • Mount: 检查 docker run -v 挂载路径是否正确。
    • User ID: 宿主机是 root (uid 0),容器内可能是 appuser (uid 1000)。容器内没权限读写挂载卷。
  2. 时区 (Timezone):
    • Docker 默认是 UTC。宿主机是 CST。
    • 排查: date 命令。解决: 挂载 /etc/localtime 或设置 TZ 环境变量。
  3. 网络隔离:
    • DNS: 容器内的 /etc/resolv.conf 可能和宿主机不同,导致解析不到内网域名。
    • Localhost: 容器内的 localhost 是容器自己,不是宿主机。连接宿主机服务需用 host.docker.internal--network host
  4. 资源限制 (Cgroups):
    • 宿主机内存很大,但容器被 k8s 限制了 Memory Limit,导致程序被 OOM Killer 杀掉(宿主机没挂,容器挂了)。

214. 一段使用多进程 DataLoader 的 PyTorch 代码,在 Windows 和 Linux 上行为不同,你会怎么定位?

核心差异:进程启动方式 (Spawn vs Fork)。

  1. Linux (fork):
    • 子进程复制父进程的内存空间(Copy-on-Write),速度快,且能共享父进程的全局变量/Socket。
  2. Windows (spawn):
    • 子进程会重新导入一遍脚本,重新初始化所有全局变量。
    • 典型报错: BrokenPipeError 或程序无限递归启动。
    • 解决: Windows 上必须把启动代码放在 if __name__ == '__main__': 之下。
    • 数据问题:spawn 模式下,传递给 DataLoader 的 dataset 对象必须是可序列化(Picklable)的,lambda 函数或某些闭包会报错。

215. 当你接手一段别人写的、缺乏注释的大型训练/推理代码,你会用什么方法快速“吃透”和重构?

三步走策略:Run it -> Trace it -> Refactor it。

  1. Run it (环境复现):
    • 先别看代码,先看能不能跑起来。配置 Docker/Conda 环境,解决依赖报错。
    • 跑通一个最小的 Demo(如 batch_size=1, max_steps=10),确保管线是通的。
  2. Trace it (动态分析):
    • 加日志/断点: 在关键入口(如 forward, data_process)打印 tensor 的 shape。
    • IDE 调试: 使用 VSCode/PyCharm 的 Debug 模式,一步步跟进(Step Into),看数据流怎么变。这是理解代码逻辑最快的方法,比干读快 10 倍。
    • 生成类图: 使用 pyreverse 等工具生成类关系图,理清架构。
  3. Refactor it (测试驱动重构):
    • 在修改前,先写一个金标准测试 (Golden Test):固定输入,记录下现在的输出。
    • 重构时,每改动一点,就跑一遍测试,确保输出和原来一致,防止改坏。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐