南湖研究院 AI Infra 一面
Q: Python 的垃圾回收算法?会循环引用吗?
Python 使用三层垃圾回收机制协同工作:
第一层:引用计数(Reference Counting)——主要机制:
1 | # 每个 PyObject 头部有一个 ob_refcnt 字段 |
优点:实时释放(引用归零即回收)、确定性析构。
缺点:无法处理循环引用、频繁修改 refcnt 有开销。
第二层:标记清除(Mark-Sweep)——解决循环引用:
1 | # 循环引用示例 |
只对容器类型做标记清除(list, dict, tuple, class instance 等),因为只有容器能引用其他对象形成循环。int, str, float 等不可能循环引用。
第三层:分代回收(Generational GC):
1 | Generation 0: 新创建的对象(GC 最频繁) |
分代的直觉:大部分对象生命周期短(临时变量),只需要频繁检查 Gen0。长期存活的对象(如模块级变量)很少成为垃圾,不需要频繁检查。
避免循环引用的方法:
1 | import weakref |
Q: 内联(inline)什么时候会影响性能?
内联将函数体直接嵌入调用点,消除函数调用开销(压栈/跳转/返回)。但过度内联会导致性能下降:
内联的正面效果:
- 消除 call/ret 指令(~3-5 cycles)
- 允许编译器跨函数边界优化(常量折叠、死代码消除)
- 消除函数调用的寄存器保存/恢复
内联的负面效果:
| 场景 | 问题 | 机制 |
|---|---|---|
| 大函数体 | 代码膨胀 (code bloat) | 每个调用点展开一份副本 → binary 体积暴增 |
| 热路径上的大函数 | I-Cache 压力 | 展开后指令总量超过 L1 I-Cache(32-64 KB)→ cache miss |
| 递归函数 | 无法完全内联 | 编译器只能展开有限层(通常 1-2 层) |
| 虚函数 | 通常无法内联 | 运行时才确定目标(除非编译器做去虚拟化) |
| 循环内大函数 | 循环体膨胀 | 展开后循环体不适合 loop unrolling/vectorization |
代码膨胀的连锁反应:
1 | 函数 A (100 条指令) 被 50 个调用点内联: |
在 CUDA/GPU 编程中的特殊考量:
- GPU kernel 中的函数调用开销更大(没有硬件 call stack 的情况下需要保存恢复寄存器)
- 但 GPU 寄存器压力大,内联可能增加寄存器使用 → occupancy 下降
- 编译器(nvcc)通常会自动内联小函数(
__device__函数默认内联) - 可用
__forceinline__或__noinline__显式控制
Q: inline 对作用域的影响?除了 inline 还有什么会内联?
inline 对作用域/链接的影响:
C++ 的 One Definition Rule(ODR)要求每个函数在整个程序中只能有一个定义。但 inline 函数获得 ODR 豁免:
1 | // header.h |
inline 的语义:
- 告诉链接器:如果多个编译单元(.o 文件)中有同名 inline 函数,它们是同一个函数,合并为一份
- 不保证内联优化:编译器可能忽略 inline 提示(函数太大时)
- 反过来,不加 inline 也可能被内联(编译器自动判断)
其他会导致内联的情况:
| 情况 | 说明 | 是否需要显式 inline |
|---|---|---|
| 类内定义的成员函数 | 隐式 inline | 不需要 |
| 模板函数 | 通常在头文件定义,编译器倾向内联 | 不需要(template 有 ODR 豁免) |
| constexpr 函数 | 编译期求值时必然”内联” | 不需要(C++17 隐式 inline) |
| 编译器自动内联(-O2/-O3) | 小函数自动内联 | 不需要 |
| LTO(Link Time Optimization) | 跨编译单元内联 | 不需要 |
| attribute((always_inline)) | GCC/Clang 强制内联 | 显式标记 |
| forceinline | MSVC / CUDA 强制内联 | 显式标记 |
编译器自动内联的判断标准:
- 函数体指令数少(通常 < 10-30 条 IR 指令)
- 调用频次高(热路径上的函数更倾向内联)
- 调用开销占比大(函数体短时 call/ret 开销占比高)
-O2比-O1更激进内联,-O3和-Ofast最激进-Os(优化大小)会抑制内联
Q: 完美转发(Perfect Forwarding)?
完美转发解决的问题:如何将函数参数按原始类型(左值/右值)透传给另一个函数,不丢失值类别信息。
问题场景:
1 | // 想实现一个通用的工厂函数 |
解决方案——万能引用 + std::forward:
1 | template<typename T, typename... Args> |
引用折叠规则(Reference Collapsing):
1 | T& & → T& (左值引用的引用 → 左值引用) |
模板参数推导 + 引用折叠的协作:
1 | template<typename T> |
std::forward 的实现:
1 | template<typename T> |
实际应用场景:
1 | // 1. emplace_back(避免额外拷贝/移动) |
Q: 右值引用的意义?
右值引用(T&&)是 C++11 引入的核心特性,解决了不必要的深拷贝问题。
核心价值——移动语义:
1 | // 场景: vector 返回大数据 |
三个核心应用场景:
容器操作加速:
1
2
3
4
5std::vector<Tensor> tensors;
tensors.push_back(Tensor(1000000)); // 临时对象是右值 → 移动(O(1))
// vector 扩容时移动所有元素(如果 move ctor 是 noexcept)
// 1000 个 Tensor 扩容: 拷贝 O(n×size) vs 移动 O(n)资源转移(unique_ptr):
1
2
3
4std::unique_ptr<Model> model = load_model("path");
std::unique_ptr<Model> server_model = std::move(model);
// 所有权从 model 转移到 server_model,model 变为空
// 不需要拷贝整个 Model 对象完美转发(配合万能引用 T&&):
1
2
3
4template<typename... Args>
auto make_kernel(Args&&... args) {
return Kernel(std::forward<Args>(args)...);
}
std::move 的本质:
1 | // std::move 不移动任何东西!它只是将左值转换为右值引用(类型转换) |
noexcept 的重要性:
- STL 容器扩容时,如果移动构造是 noexcept,则使用移动
- 如果移动构造可能抛异常,STL 退回使用拷贝(保证异常安全)
- 因此移动构造函数务必加
noexcept
Q: FasterTransformer(FT)框架的特点?
FasterTransformer 是 NVIDIA 推出的高性能 Transformer 推理库(现已演进为 TensorRT-LLM),代表了 CUDA 原生 LLM 推理的极致优化水平。
核心特点:
| 维度 | 特点 | 详情 |
|---|---|---|
| 语言 | 纯 C++/CUDA | 无 Python 开销,适合生产部署 |
| 算子优化 | 极致手写 CUDA kernel | 融合 MHA、融合 LayerNorm+Add、融合 GEMM+Bias+Act |
| 并行支持 | TP + PP | 多卡推理,NVLink/NCCL 通信 |
| 量化 | INT8/FP8 | 借助 Tensor Core 加速 |
| 模型支持 | GPT/BERT/T5/LLaMA/Bloom 等 | 主流架构预实现 |
| 内存管理 | 手动 memory pool | 预分配+复用,减少运行时分配 |
FT 的优化手段(后续被 TensorRT-LLM 继承):
1 | 1. 算子融合: |
FT → TensorRT-LLM 的演进:
| 维度 | FasterTransformer | TensorRT-LLM |
|---|---|---|
| 接口 | C++ API | Python + C++ plugin |
| 图优化 | 手动 | 基于 TensorRT 编译器 |
| 动态 batch | 基础支持 | In-flight Batching (Continuous Batching) |
| 量化 | INT8 | INT8/INT4/FP8 + AWQ/GPTQ/SmoothQuant |
| KV Cache | 连续分配 | 类 PagedAttention 分页管理 |
| 投机解码 | 无 | 内置支持 |
| 多模态 | 有限 | 支持 VLM |
| 维护 | 已停止 | 积极维护 |
为什么 FT 被替代:
- C++ 纯手动实现新模型开发周期长(每支持一个新模型需要数周)
- 缺乏动态 batch/PagedAttention 等现代推理框架特性
- TensorRT-LLM 继承了 FT 的所有优化 + 添加了编译器自动化 + Python 易用性
Q: 线程同步的方式?
CPU 线程同步机制:
| 方式 | 原理 | 适用场景 | 开销 |
|---|---|---|---|
| Mutex(互斥锁) | 同时只有一个线程进入临界区 | 通用互斥 | 无竞争 ~25ns,竞争 ~μs |
| Spinlock(自旋锁) | 忙等待(循环 CAS) | 临界区极短(< 1μs) | CPU 占用高 |
| RWLock(读写锁) | 多读单写 | 读多写少 | 略高于 mutex |
| Condition Variable | 等待条件 + 通知唤醒 | 生产者-消费者 | park/unpark ~μs |
| Semaphore(信号量) | 计数型,允许 N 个线程同时访问 | 限流/资源池 | 类似 mutex |
| Barrier(屏障) | 所有线程到达后才继续 | 分阶段并行计算 | 取决于最慢线程 |
| Atomic(原子操作) | 无锁的 CAS/fetch_add | 计数器、标志位 | ~1-10ns |
选择决策树:
1 | 需要互斥? |
CUDA 中的同步机制:
| 级别 | 方式 | 作用域 | 使用场景 |
|---|---|---|---|
| Warp 内 | 隐式同步(SIMT lockstep) | 32 线程 | 同一 warp 天然同步 |
| Warp 内 | __syncwarp(mask) |
指定线程 | divergent path 后重新同步 |
| Block 内 | __syncthreads() |
全 block | shared memory 读写同步 |
| Grid 内 | cooperative_groups |
全 grid | persistent kernel 通信 |
| Device 级 | cudaDeviceSynchronize() |
host 等待 device | 确保所有 kernel 完成 |
| Stream 级 | cudaStreamSynchronize() |
指定 stream | 等待特定 stream 完成 |
| Event | cudaEventRecord/Wait |
跨 stream | stream 间依赖 |
__syncthreads() 使用规则:
1 | // 正确: 所有线程都到达同一个 syncthreads |
Q: CUDA Stream 有什么要求?如何实现设备级重叠?
CUDA Stream 基本概念:
Stream 是 GPU 上的有序命令队列。同一 stream 内的操作按顺序执行,不同 stream 间的操作可以并发。
核心要求:
异步操作需指定 stream:
1
2
3
4
5
6
7cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
// 指定 stream 的异步操作
cudaMemcpyAsync(d_a, h_a, size, cudaMemcpyHostToDevice, stream1);
kernel<<<grid, block, 0, stream2>>>(d_b);异步传输需要 Pinned Memory:
1
2
3
4
5
6
7// 只有 pinned memory 才能真正异步传输
float* h_data;
cudaHostAlloc(&h_data, size, cudaHostAllocDefault); // pinned
cudaMemcpyAsync(d_data, h_data, size, H2D, stream); // 真正异步
// 普通 malloc 的内存: cudaMemcpyAsync 实际会退化为同步
// 因为 DMA 引擎需要物理地址(pinned memory 锁页不换出)Default Stream (stream 0) 的隐式同步:
- 如果不指定 stream,操作在 default stream 执行
- Default stream 会隐式与所有其他 stream 同步(blocking)
- 使用
--default-stream per-thread编译可避免
设备级重叠(计算与传输 Overlap):
GPU 有独立的硬件引擎:
1 | 硬件引擎: |
经典三级流水线:
1 | // 将数据分为 N 份,用 3 个 stream 流水线处理 |
依赖管理——CUDA Event:
1 | cudaEvent_t event; |
实际收益(以 PCIe 4.0 + A100 为例):
1 | 不重叠: |
Q: Git 协作流程?
标准 Feature Branch 工作流:
1 | main ─────●────────●──────────●──────────── (始终可部署) |
完整流程:
1 | # 1. 创建 feature 分支 |
冲突解决:
1 | # 方式 1: Rebase(推荐,线性历史) |
Commit Message 规范(Conventional Commits):
1 | feat: 新功能 |
Q: 手撕:智能指针(shared_ptr)?
(编程题)
Q: 手撕:前 K 大的元素?
(编程题)