1000道大模型算法工程师面试题汇总(1-35部分 · 将持续更细)

这是第十四部分的详细参考答案。这部分考察的是 SRE(站点可靠性工程)和运维实战。对于生产环境的大模型服务,"跑起来"只是第一步,"稳得住"才是真正的本事。


第十四部分:稳定性保障与故障排查 (SRE/Ops) (276-300)

276. 线上服务突然 OOM,但是显存监控显示还有剩余,这通常是什么原因?(显存碎片?PyTorch 缓存未释放?)

答案:

  • 原因: 显存碎片化 (Fragmentation)
    • PyTorch 的 CachingAllocator 会申请大块显存并切分使用。如果程序中频繁申请不同大小的 Tensor(特别是动态 Shape 推理),会导致大块显存被切碎。
    • 当需要一个连续的大块显存(比如 Prefill 阶段的 Attention 矩阵)时,虽然总剩余显存够,但没有一块足够大的连续空间,就会报 OOM。
  • 解决:
    • 设置 PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 来减少碎片。
    • 在代码中适时调用 torch.cuda.empty_cache()(注意这会拖慢速度,仅在必要时用)。
277. 遇到过“僵尸进程”吗?主进程挂了,但 GPU 显存还被子进程占着,导致重启失败。你的启动脚本里怎么处理这种情况?

答案:

  • 现象: Docker 容器重启了,但 GPU 显存没释放,因为之前的 Worker 进程(子进程)变成了孤儿进程,被 Init 进程接管但没被杀掉。
  • 处理:
    1. 使用 tini 在 Dockerfile 中使用 ENTRYPOINT ["/usr/bin/tini", "--", "python", "app.py"]。Tini 作为 PID 1 进程,负责正确回收所有僵尸子进程。
    2. 启动脚本 Trap: 在 shell 脚本里写 trap 'kill $(jobs -p)' EXIT,确保主进程退出时广播 Kill 信号给所有子进程。
278. 你们的健康检查(Health Check)是只查 TCP 端口通不通,还是会真实地发一个推理请求进去测?

答案:

  • 策略: 分层检查
    • Liveness Probe(存活检测): 查 TCP 端口或简单 HTTP Ping。只要进程还在,就不杀 Pod。
    • Readiness Probe(就绪检测): 必须发真实的推理请求(通常是一个固定的、极短的 Prompt,如 “hi”)。
    • 原因: 大模型服务经常出现“死锁”情况(Python GIL 锁死或 CUDA 挂起),端口是通的,但无法处理请求。只有真实推理才能判断服务是否可用,从而决定是否切断流量。
279. 服务运行几天后,推理速度越来越慢,重启就好了,这大概率是什么泄露了?(CPU 内存?文件句柄?还是 KV Cache 管理 Bug?)

答案:

  • 概率最高: KV Cache 碎片化Python 对象泄露
    • KV Cache: 如果是自己实现的 PagedAttention 逻辑,可能存在 Page 回收不彻底的问题,导致可用 Page 越来越少,Cache Miss 变高,速度变慢。
    • Python 泄露: 比如在全局 List 里记录了每次请求的 Log 或 Latency 对象,几天下来积攒了几百万个对象,GC 扫描压力变大,导致 CPU 占用升高,拖慢整体调度。
280. 当 GPU 温度过高触发降频保护时,你们的监控系统能第一时间报警吗?是用 Prometheus 抓的哪个指标?

答案:

  • 工具: DCGM Exporter (NVIDIA Data Center GPU Manager)。
  • 指标:
    1. DCGM_FI_DEV_GPU_TEMP:GPU 温度。
    2. DCGM_FI_DEV_CLOCK_THROTTLE_REASONS最关键。这个值非 0 代表正在降频。
  • 报警: 如果 Temp > 82°C 或者 Throttle > 0,立即触发 P1 报警给运维,检查机房空调或风扇。
281. 多个模型共享 GPU 显存时(比如一个 Embedding 一个 LLM),怎么防止一个把另一个挤挂?(CUDA MPS 用过吗?)

答案:

  • 风险: 默认情况下,CUDA Context 之间没有内存隔离。Embedding 模型突然来个大 Batch,可能把 LLM 的显存挤爆。
  • MPS (Multi-Process Service): 主要是解决计算资源的时间切片问题(提升小模型并发利用率),但不能强隔离显存
  • 最佳实践: 不要共享。
    • 如果非要共享,必须在代码层面(TensorFlow 的 memory_fraction 或 PyTorch 的 max_split_size 间接控制)严格限制每个进程的显存上限,或者使用 MIG (Multi-Instance GPU) 从硬件层面切分 A100。
282. 遇到过 K8s Pod 处于 Pending 状态,提示“没有足够的 GPU 资源”,但实际上有卡空闲,这是调度器哪里没配对?

答案:

  • 原因: K8s 调度器看的是 Requests,不是实际利用率。
    • 即使物理卡是空闲的,如果之前调度的 Pod 在 YAML 里申领了这张卡(nvidia.com/gpu: 1),K8s 账本上这就已经没了。
  • 特殊情况:
    • Taints/Tolerations: 节点被打上了污点(如 NoSchedule),新 Pod 没有对应的容忍度。
    • Device Plugin 问题: 某个 Pod 异常退出了,但 Device Plugin 没有向 Kubelet 汇报资源释放,导致 K8s 以为卡还在被占用(Ghost Pod)。重启 kubelet 或 device plugin pod 可解。
283. 生产环境的日志量太大了(全量的 Input/Output),把磁盘写满了怎么办?你们有做采样记录(Sampling Logging)吗?

答案:

  • 原则: 生产环境严禁全量打印 Input/Output Payload。
    • 既有隐私合规风险,又会撑爆磁盘(JSON 很大)。
  • 措施:
    1. Sampling: 只记录 1% 的请求详情用于抽检。
    2. Log Rotation: Docker 配置 max-size: 100m, max-file: 3
    3. 元数据记录: 对所有请求,只记 TraceID, Token_Count, Latency, Status_Code,这些数据量很小但足够分析性能。
284. 在 Docker 里,/dev/shm(共享内存)太小会导致 DataLoader 报错,你一般给容器挂载多大的 shm?

答案:

  • 报错: Bus error (core dumped)
  • 默认值: Docker 默认为 64MB,跑 PyTorch 肯定不够。
  • 设置:
    • Docker: --shm-size=8g
    • K8s: 挂载一个 emptyDir 卷,并设置 medium: Memory。大小建议设置为物理内存的 30%
285. 如果模型推理时出现了 CUDA Error: device-side assert triggered,这种报错一般不准,你怎么定位真实的错误位置?

答案:

  • 原因: GPU 是异步执行的。当 CPU 收到报错时,GPU 可能已经跑过了好几个 Kernel,堆栈对不上。
  • 定位: 设置环境变量 CUDA_LAUNCH_BLOCKING=1
    • 这会强制 CPU 等待每个 GPU Kernel 执行完(同步模式)。
    • 虽然程序会变慢,但报错时的 Python 堆栈会准确指向出错的那一行代码(通常是 Embedding 层索引越界,即 Input ID 超过了 Vocab Size)。
286. 客户端突然断开连接,后端正在跑的推理任务能自动 Cancel 吗?还是必须等它跑完?这对资源浪费影响大吗?

答案:

  • 现状: 如果不做特殊处理,后端通常会跑完,严重浪费算力。
  • 处理:
    • HTTP/FastAPI: 监听 client_disconnect 事件。
    • Asyncio: 捕获 asyncio.CancelledError
    • vLLM 集成: 调用 engine.abort_request(request_id)
  • 必须做: 对于生成长文本的任务,用户可能等了 5 秒没耐心就刷新的,如果不 Cancel,GPU 还在傻傻生成剩下的 2000 字,纯属浪费。
287. 你们做没做过“降级预案”?比如 A100 集群全挂了,能不能自动切到 T4 集群或者 CPU 集群顶一会儿?

答案:

  • 做过。 高可用(HA)的核心。
  • 链路: 网关层(Nginx/Kong)配置 Upstream Group。
    • Primary: A100 集群。
    • Backup: T4 量化集群(速度慢点,但能用)。
    • Fallback: 甚至可以切到 Azure OpenAI/DeepSeek API(作为最后的兜底,虽然贵)。
  • 逻辑: 当 Primary 返回 5xx 错误比例超过阈值,自动切 Backup。
288. 模型加载时间太长(比如 2 分钟),导致 K8s 认为服务启动失败(Liveness Probe 失败)并杀掉重启,这怎么解?

答案:

  • 配置错误: 把 Liveness Probe 当 Startup Probe 用了。
  • 修正: 使用 Startup Probe(启动探针)
    • 配置 failureThreshold: 30, periodSeconds: 10。这意味着 K8s 会给容器 300 秒的时间去启动。
    • 在 Startup Probe 成功之前,Liveness Probe 不会启用。这样就避免了“还没加载完就被杀掉”的死循环。
289. 遇到过 PyTorch 版本和 CUDA 版本不匹配导致的奇怪 Bug 吗?比如算子计算结果全为 0。

答案:

  • 遇到过。 或者是报错 symbol lookup error
  • 场景: 宿主机驱动是 CUDA 11.4,Docker 里装了 PyTorch (CUDA 12.1 版)。虽然通常兼容,但在某些特定算子(如 FlashAttention)编译时依赖的 Driver 版本如果不一致,会出灵异现象。
  • 解决: 严格使用 NVIDIA 官方的 NGC 容器(如 nvcr.io/nvidia/pytorch:23.10-py3),这里的 Torch 和 CUDA 是经过严格测试配对的。
290. 在高并发下,Python 的 GC(垃圾回收)会导致明显的 Stop-the-world 顿卡吗?你们有没有调整过 GC 阈值?

答案:

  • 会。 当内存中对象极多时,GC 扫描会暂停主线程几十毫秒,造成 Latency 抖动(P99 飙升)。
  • 优化:
    1. 禁用自动 GC: 在推理主循环开始前 gc.disable()
    2. 手动 GC: 在处理完一个 Batch 的请求后,或者显存/内存达到警戒线时,手动调用 gc.collect()
    3. 调整阈值: gc.set_threshold(700, 10, 10) 调大一点,减少扫描频率。
291. 怎么保证 API Key 不被刷爆?你们在网关层做了什么样的 Rate Limit?是针对 IP 还是针对 User ID?

答案:

  • 针对 User ID / Key。 针对 IP 是没用的(学校/公司出口 IP 相同,或者攻击者用代理池)。
  • 算法: 令牌桶 (Token Bucket)滑动窗口 (Sliding Window)
  • 存储: 使用 Redis + Lua 脚本 实现原子计数。
  • 策略: 分级限流。
    • Free User: 10 RPM (Requests Per Minute).
    • Pro User: 60 RPM.
    • Total Global Limit: 保护后端不被打死。
292. 如果发现某个 Worker 进程 CPU 占用率 100% 但 GPU 利用率 0%,这通常是卡在什么地方了?(死循环?锁竞争?)

答案:

  • 排查: 使用 py-spy dump --pid [pid] 查看 Python 堆栈。
  • 常见原因:
    1. Tokenizer 死循环: 遇到了极其特殊的 Unicode 字符串,正则匹配回溯卡死。
    2. GIL 锁竞争: 多线程处理 Log 或 Metrics 时死锁。
    3. 后处理逻辑: 比如在 Python 里做了一个巨大的 List 拼接或字符串处理,把 CPU 跑满了,还没轮到 GPU 推理。
293. 你们的模型权重文件是放在 Docker 镜像里,还是启动时从 S3 拉取?哪种启动更快?

答案:

  • 方式: 启动时挂载(PVC/HostPath/NAS)。
  • 对比:
    • 打入镜像: 镜像会变成 20GB+。Pull 镜像极其慢,且每次更新模型都要重新 Build,分发效率极低。
    • 挂载存储: 镜像只有代码(500MB)。模型文件存在 NFS/Ceph 上,Pod 启动挂载即可读取。结合 Fluid/Alluxio 缓存,启动最快。
294. 遇到过“显存泄漏”排查最难的一次是什么情况?最后是用 torch.cuda.memory_summary() 查出来的吗?

答案:

  • 案例: 一个由 torch.compile (PyTorch 2.0) 引发的泄露。
  • 过程: memory_summary 显示显存被占用但没被 Tensor 引用。
  • 真凶: 是计算图缓存(Graph Cache)。因为输入的 Shape 极其多变(动态 Shape),导致 PyTorch 2.0 不断编译新的 Kernel 并缓存,撑爆了显存。
  • 解决: 固定 Input Shape 分桶,或者限制 Compile 的 Cache Size。
295. 生产环境你们敢用 Spot Instances(抢占式实例)来跑推理吗?如果机器被回收了,怎么保证服务不中断?

答案:

  • 敢用。 为了省钱(便宜 60%-80%)。
  • 保障机制:
    1. 混合部署: Spot 实例和 On-Demand 实例混跑(如 80% Spot + 20% Stable)。
    2. 监听中断信号: 云厂商回收前 2 分钟会发信号。程序捕获后,将节点设为 Cordon(不调度新流量),并利用这 2 分钟处理完 In-flight 请求,然后优雅退出 (Graceful Shutdown)。
    3. 快速补货: K8s Cluster Autoscaler 自动购买新机器。
296. 怎么监控“显存带宽利用率”?nvidia-smi 看不到这个,你们用 Nsight Systems 还是 DCGM?

答案:

  • 线上监控:DCGM Exporter。指标是 DCGM_FI_DEV_MEM_COPY_UTIL(显存复制利用率)。
  • 深度分析:Nsight Systems (nsys)。DCGM 只能看大概,nsys 能看到微秒级的 Kernel 显存读写情况,判断是 H2D 慢还是 D2D 慢。
297. 遇到过 NCCL timeout 错误吗?在跨机训练或推理时,这通常是防火墙问题还是网卡问题?

答案:

  • 原因: 90% 是 网络不可达
  • 检查点:
    1. 防火墙/安全组: NCCL 使用随机高位端口。必须开放所有 TCP 端口或指定范围。
    2. 网卡绑定: 多网卡机器上,NCCL 抓错了网卡(比如抓到了 Docker 的虚拟网卡)。需设置 NCCL_SOCKET_IFNAME=eth0
    3. MTU 问题: 某些交换机开启了 Jumbo Frame (MTU 9000),但服务器没配,导致大包丢包。
298. 你们的 Python 服务是用多进程(Multiprocessing)还是多线程(Multithreading)?在大模型推理场景下,GIL 锁的影响还大吗?

答案:

  • 选型: 多进程(Process-based)。
  • 原因:
    • 推理不仅仅是 GPU 跑,还有大量的 TokenizationDetokenization,以及 HTTP 协议解析。这些都是 CPU 密集型操作。
    • 在 GIL 限制下,多线程跑这些会争抢 CPU,甚至导致 GPU 等待 CPU(Starvation)。
    • vLLM 内部就是通过 Ray 或 Python Multiprocessing 启动多个 Worker 来规避 GIL。
299. 如果 Update 模型版本,怎么做到“无损滚动更新”?旧的请求还在处理,新的 Pod 刚启动,怎么平滑切换?

答案:

  • K8s RollingUpdate:
    • 设置 maxSurge: 1, maxUnavailable: 0(先起新,再杀旧)。
  • 关键点: Graceful Shutdown
    • 旧 Pod 收到 SIGTERM 后,从 Service Endpoint 摘除(不接新客)。
    • 应用层捕获信号,等待所有正在推理的请求(In-flight Requests)跑完,再退出进程。
    • K8s 的 terminationGracePeriodSeconds 要设得足够长(比如 120s),给它收尾的时间。
300. 终极一问: 如果现在线上服务全崩了,老板就在你后面站着,你恢复服务的标准 SOP(操作流程)前三步是什么?

答案:

  1. 止损(切流/熔断): 不要先查 Bug!先切断流量或者降级(切到备用集群/Azure API)。如果是因为发布导致的,立即一键回滚 (Rollback) 到上个镜像版本。保证用户先能用,或者看到友好的降级提示。
  2. 扩容/重启: 如果是突发流量打挂的,立即扩容(Scale Out)。如果是内存泄露导致的假死,立即重启所有 Pod(Restart)。暴力但有效。
  3. 保留现场: 在重启前,务必保留 1-2 个故障 Pod(Remove Label 让它脱离 Service 但不被杀掉),以便后续抓 Core Dump 或分析日志查根因。
  • (回答要体现:业务连续性第一,查 Bug 第二的运维原则)

1000道大模型算法工程师面试题汇总(1-35部分 · 将持续更细)

Logo

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

更多推荐