AI Infra 综合面经题库 (5)


Q: Python全局解释器锁(GIL)是什么?

GIL(Global Interpreter Lock)是CPython解释器中的一把全局互斥锁,它确保同一时刻只有一个线程执行Python字节码。GIL的设计初衷是保护CPython的引用计数(reference counting)内存管理机制——因为引用计数的增减不是原子操作,没有GIL的话多线程并发修改引用计数会导致内存泄漏或段错误。

GIL的工作机制

  • CPython中每个线程在执行前必须获取GIL,执行一定数量的字节码指令(Python 3.2+ 默认每5ms)后释放GIL,让其他线程有机会获取
  • IO操作(网络、磁盘)和部分C扩展调用时会主动释放GIL

性能影响

任务类型 GIL影响 原因
CPU密集型多线程 严重,无法利用多核 同一时刻只有一个线程执行字节码
IO密集型多线程 影响小 线程等待IO时释放GIL
单线程 无影响 不存在锁竞争

绕过GIL的方式

  1. 多进程(multiprocessing):每个进程有独立的GIL,可以真正并行。代价是内存开销大、进程间通信成本高
  2. C扩展释放GIL:NumPy/SciPy的计算密集操作在C代码中执行时会释放GIL(Py_BEGIN_ALLOW_THREADS宏)
  3. 使用其他解释器:PyPy(有GIL但JIT更快)、Jython/IronPython(无GIL)
  4. GIL-free CPython:Python 3.13+ 实验性支持 --disable-gil 编译选项(PEP 703),通过细粒度锁和偏向引用计数替代GIL

面试陷阱:GIL并不意味着Python多线程完全无用——IO密集型场景(爬虫、网络服务)多线程仍然有意义;且GIL是CPython特有的实现细节,不是Python语言规范的一部分。


Q: Python不同进程之间如何通信(IPC)?

Python进程间通信(Inter-Process Communication)方式丰富,选择取决于数据量、延迟要求和场景复杂度:

1. multiprocessing.Queue / Pipe

  • Queue:多生产者-多消费者模型,底层基于管道+锁实现,线程/进程安全
  • Pipe:双向管道,适合两个进程之间的简单通信,性能更好但只支持两端
  • 数据通过pickle序列化传输,大对象序列化开销大

2. 共享内存

  • multiprocessing.Value/Array:适合简单数据类型的共享(基于mmap)
  • multiprocessing.shared_memory(Python 3.8+):创建命名共享内存段,支持任意二进制数据,零拷贝访问
  • 性能最高(无序列化/拷贝),但需要自行管理同步(Lock)

3. Manager对象

  • 提供共享的dict/list/Namespace等高级数据结构
  • 底层通过代理进程(server process)实现,每次读写都是RPC调用
  • 方便但性能差(每次操作都有IPC开销),适合低频访问

4. Socket / 网络通信

  • 支持跨机器通信,灵活性最高
  • 可用TCP/UDP/Unix Domain Socket,后者无网络栈开销
  • 需要自行处理协议、序列化、连接管理

5. 文件 / mmap

  • 内存映射文件(mmap):将文件映射到多个进程的地址空间,修改对所有进程可见
  • 适合大数据量共享(如大tensor),性能接近共享内存
  • 可用于不同语言进程间的通信

6. torch.distributed / NCCL / Gloo

  • PyTorch分布式通信后端,专为深度学习设计
  • NCCL:GPU-GPU通信最优选择,支持NVLink/PCIe/InfiniBand
  • Gloo:CPU通信后端,支持各种集合通信原语
  • 适用于多机多卡训练场景,原生支持AllReduce/Broadcast/AllGather等

选择建议

场景 推荐方式
简单任务结果传递 Queue
大numpy数组共享 shared_memory / mmap
分布式训练 torch.distributed (NCCL)
跨机器通信 Socket / gRPC
简单状态共享 Manager(低频)/ Value(高频)

Q: AllReduce通信开销具体是怎么计算的?

Ring AllReduce是分布式训练中最常用的梯度同步算法,其通信开销可以精确计算:

Ring AllReduce的两阶段

  1. Reduce-Scatter阶段:将数据分为N份(N=GPU数),每个GPU发送(N-1)次,每次发送M/N数据。最终每个GPU持有完整数据的1/N(已归约)
  2. All-Gather阶段:每个GPU将自己归约好的1/N数据广播给其他所有GPU,经过(N-1)步后所有GPU拥有完整结果

通信量计算

  • 每GPU总发送量:(N-1)/N * M(Reduce-Scatter) + (N-1)/N * M(All-Gather) = 2*(N-1)/N * M
  • 每GPU总接收量:同样是 2*(N-1)/N * M
  • 当N→∞时,通信量趋近2M与GPU数量基本无关——这是Ring AllReduce的核心优势(带宽最优)

完整时间模型(alpha-beta模型)

1
T_ring = 2*(N-1) * α + 2*(N-1)/N * M / β

其中α=延迟(latency),β=带宽(bandwidth),M=消息大小

与其他AllReduce算法对比

算法 通信量(每GPU) 延迟步数 适用场景
Ring AllReduce 2*(N-1)/N * M ≈ 2M 2*(N-1) N较小,消息大
Tree AllReduce 2*M 2*log(N) N很大,消息小
Recursive Halving-Doubling 2*M 2*log(N) N为2的幂

关键洞察

  • Ring AllReduce是带宽最优延迟非最优的——延迟随N线性增长
  • 对于大模型梯度(数百MB~GB级),带宽是主要瓶颈,Ring AllReduce最合适
  • 对于小消息(如标量同步),Tree AllReduce更优(延迟O(logN))
  • 实际NCCL会根据消息大小自动选择算法(小消息用Tree,大消息用Ring)

实际带宽参考:NVLink 4.0单向带宽450 GB/s(A100为600 GB/s双向),PCIe 5.0约64 GB/s,InfiniBand NDR 400Gb/s≈50 GB/s


Q: tensor.view和tensor.contiguous的区别?

这两个操作涉及PyTorch tensor的内存布局概念:

**tensor.view(shape)**:

  • 返回原tensor的不同shape的视图(view),与原tensor共享底层数据(无数据拷贝)
  • 前提条件:tensor在内存中必须是连续的(contiguous),即逻辑顺序与物理存储顺序一致
  • 如果tensor不连续(如经过transpose/permute后),调用view会抛出RuntimeError
  • 修改view返回的tensor会同时修改原tensor(共享内存)

**tensor.contiguous()**:

  • 返回内存连续的tensor
  • 如果已经连续→返回自身(无拷贝,O(1))
  • 如果不连续→分配新内存并拷贝数据使其在内存中按行优先顺序排列

什么时候tensor会不连续?

1
2
3
4
x = torch.randn(3, 4)        # 连续的
y = x.t() # transpose后不连续(stride改变,数据未移动)
z = x.permute(1, 0) # 同上
w = x[:, ::2] # 切片后可能不连续

**tensor.reshape() vs view()**:

  • reshape:自动处理非连续情况——如果连续则等价于view(无拷贝),如果不连续则先contiguous再view(有拷贝)
  • view:严格要求连续,不连续时报错——好处是你能明确知道是否发生了数据拷贝

性能建议

  • 如果确定tensor连续(常见情况),用view更明确
  • 如果不确定连续性且不关心是否拷贝,用reshape更安全
  • 常用模式:tensor.contiguous().view(new_shape)
  • 注意:频繁的contiguous()调用会产生额外显存分配和拷贝,应在性能关键路径上避免

Q: 张量并行(Tensor Parallelism)中先列切和先行切有什么区别?

对于线性层 Y = XA(X为输入,A为权重矩阵):

列切分(Column Parallel Linear)

  • 将权重A沿列维度切分为 [A₁, A₂, …, Aₙ],分布到N个GPU
  • 每个GPU接收完整的输入X,计算 Yᵢ = X × Aᵢ,得到输出的一部分列
  • 输出 Y = [Y₁, Y₂, …, Yₙ] 需要 All-Gather 拼接(如果直接使用),或者直接传给下一层的行切分(无需通信)
  • 适合作为两层MLP的第一层

行切分(Row Parallel Linear)

  • 将权重A沿行维度切分为 [A₁; A₂; …; Aₙ]
  • 输入X也需要对应切分(或上一层的列切分输出就是天然切分好的)
  • 每个GPU计算部分和 Yᵢ = Xᵢ × Aᵢ
  • 最终输出需要 All-Reduce(求和)得到完整结果 Y = ΣYᵢ
  • 适合作为两层MLP的第二层

Megatron-LM的经典设计

1
2
3
4
5
MLP: Input → [Column Parallel] → GeLU → [Row Parallel] → Output
无通信 All-Reduce(f=identity, g=all-reduce)

Attention: Input → QKV [Column Parallel] → Attention → Output Proj [Row Parallel] → Output
无通信 All-Reduce

关键优化:通过配对使用(列切分+行切分),每个Transformer子层只需1次All-Reduce(前向1次+反向1次),最小化通信次数。

通信量分析

  • 每层前向:2次All-Reduce(Attention + MLP),每次通信量 = hidden_size × seq_len × batch_size × dtype_size
  • 对于hidden_size=8192, seq_len=2048, batch=1, FP16:每次All-Reduce约32MB
  • TP通信在前向计算的关键路径上,必须低延迟→所以TP通常只在NVLink连接的GPU间使用

列切分 vs 行切分对比

特性 列切分 行切分
输入 完整(需broadcast/identity) 切分的
输出 切分的 完整(需All-Reduce)
前向通信 输出需gather 输出需all-reduce
反向通信 梯度需reduce-scatter 梯度需all-gather
激活函数 可在切分状态下做(非线性) 在聚合后做

Q: 手撕:数值的整数次方(快速幂)?

(编程题)


Q: 手撕:实现Graph Fusion?

(编程题)