北极雄芯 AI Infra 一面
Q: 对AI编译器了解吗?
AI 编译器的核心使命是将高层模型表示自动转换为特定硬件的高效执行代码,弥合框架(PyTorch/TF)与硬件(GPU/NPU/CPU)之间的性能鸿沟。
代表框架:
- TVM:Apache 开源,Relay IR(图级) + TIR(算子级) + Auto Schedule(Ansor)。支持多后端,社区活跃。
- XLA(Google):HLO IR + 算子融合 + 布局优化。TensorFlow/JAX 的默认编译器。
- Triton(OpenAI):Python DSL 到 GPU kernel,用 tile 抽象简化 kernel 编写,自动处理 tiling/vectorization/memory hierarchy。
- torch.compile / TorchInductor:PyTorch 2.0+ 的默认编译器,基于 FX graph + Triton/C++ 后端。
优化层次:
- 图级优化:算子融合(减少 kernel launch 和中间读写)、常量折叠(编译期计算不变量)、布局转换(NCHW->NHWC 适配硬件)、死代码消除
- 算子级优化:循环变换(split/reorder/tile/vectorize/unroll)、内存层次管理(cache_read/write_at)、auto-tuning(在参数空间搜索最优配置)
- 代码生成:输出 CUDA PTX / LLVM IR / 目标汇编
价值:一次编写模型,自动适配多种硬件。尤其对新硬件(国产 AI 芯片)意义重大——无需为每个算子手写优化 kernel。
Q: 线程同步方法有哪些?
多线程编程中避免数据竞争和确保执行顺序的基本机制:
互斥锁(Mutex):最基本的同步原语。一次只允许一个线程进入临界区。获取不到时线程阻塞(让出CPU)。适合一般临界区保护。开销:~25ns(无竞争)/ ~数us(有竞争,涉及上下文切换)。
读写锁(RWLock):多个读者可并发,写者独占。适合读多写少场景(如配置读取)。但写者可能饥饿(持续有读者时写者拿不到锁)。
条件变量(Condition Variable):线程等待特定条件成立(
wait释放锁并挂起),其他线程修改条件后notify唤醒等待者。典型场景:生产者-消费者队列。信号量(Semaphore):广义互斥锁,控制同时访问资源的线程数(count>1)。适合连接池、线程池限流。
自旋锁(Spinlock):获取不到时忙等(循环 CAS),不让出 CPU。适合锁持有时间极短(< 几百ns)的多核场景。单核不适用。
屏障(Barrier):等待所有线程都到达同步点后才继续。适合分阶段并行计算(如 OpenMP 的隐式 barrier)。
选择原则:临界区短且竞争激烈用自旋锁;临界区长用互斥锁;读多写少用读写锁;需要等待条件用条件变量。
Q: 单例模式?
确保一个类全局只有一个实例,并提供全局访问点。常用于日志器、配置管理、连接池等全局唯一资源。
实现方式:
Meyers’ Singleton(C++11 推荐):
1
2
3
4
5
6
7
8
9
10
11class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};利用 C++11 标准保证的局部静态变量线程安全初始化(Magic Static),编译器在底层使用 double-checked locking + 内存屏障实现。
饿汉式:程序启动时创建(全局变量初始化阶段)。天然线程安全但可能浪费资源(即使不使用也创建),且存在 SIOF 风险。
懒汉式 + 双检查锁:需配合
std::atomic和内存序防止指令重排。C++11 后不推荐(Meyers 方案更简洁安全)。
Q: 双检查锁(Double-Checked Locking)的隐患?
隐患:CPU 和编译器的指令重排序可能导致对象未完全构造就被其他线程使用。
instance = new Singleton() 实际包含三步:
- 分配内存
- 在内存上调用构造函数
- 将地址赋值给 instance 指针
编译器/CPU 可能将步骤重排为 1->3->2。此时另一个线程看到 instance != nullptr 就直接使用,但对象构造尚未完成——导致 UB(访问未初始化的成员变量)。
正确的解决方案:
- 使用
std::atomic<Singleton*>配合memory_order_acquire/release,确保写入顺序对其他线程可见 - 或直接用 C++11 的局部静态变量(Meyers’ Singleton),由编译器保证正确性
- 在 Java 中使用
volatile关键字防止重排
Q: 还知道哪些设计模式?
创建型:
- 工厂模式:封装对象创建逻辑,根据参数返回不同子类实例。解耦创建和使用。
- 建造者模式:分步骤构建复杂对象(如 protobuf Message 的 Builder)。
结构型:
- 适配器模式:将不兼容接口转换为期望的接口(如 STL 的 stack/queue 适配 deque)。
- 装饰器模式:动态添加职责而不修改原类(如 Java IO 的 BufferedReader 包裹 FileReader)。
行为型:
- 观察者模式:一对多依赖,状态变化时通知所有观察者(如事件系统/Pub-Sub)。
- 策略模式:算法族可互换(如排序策略、路由策略),通过接口多态实现运行时切换。
Q: malloc的详细原理?
glibc 的 malloc 实现(ptmalloc2):
小块内存(<128KB):通过 brk() 系统调用扩展进程堆顶(program break)。内部维护多级空闲链表:
- Fastbin(<= 64B):单链表 LIFO,分配/释放极快(无合并),线程私有
- Smallbin(< 512B):按大小精确分级的双链表
- Largebin(>= 512B):按大小范围分级的双链表 + 跳表加速
大块内存(>=128KB):通过 mmap() 匿名映射直接分配。释放时 munmap() 归还 OS。
Chunk 结构:每个分配块前有 header(8-16B),记录大小、使用标志、前后块信息。空闲时相邻块会被合并(减少碎片)。
多线程优化:每个线程有独立的 arena(内存池),减少锁竞争。tcmalloc/jemalloc 进一步优化了多线程性能。
Q: NVIDIA Profiler工具?
NVIDIA 提供两个层级的性能分析工具:
Nsight Compute(kernel 级精细分析):
- SM Throughput / Occupancy:SM 利用率和活跃 warp 比例
- Memory Throughput:L1/L2/HBM 各级带宽利用率
- Compute Throughput:FP32/FP16/Tensor Core 利用率
- Roofline Model:直观显示 kernel 是 compute-bound 还是 memory-bound
- Warp Stall Reasons:分析线程停滞原因(等内存/等同步/等指令)
Nsight Systems(系统级 timeline 分析):
- CPU-GPU 交互时序:看 kernel launch、内存传输、CPU 阻塞
- 多 stream 并行度:是否充分利用异步并发
- CUDA API 调用开销:定位不必要的同步或低效 API 使用
使用建议:先用 Nsight Systems 定位热点 kernel 和系统瓶颈,再用 Nsight Compute 深入分析具体 kernel 的优化方向。
Q: PyTorch底层原理了解哪些?
PyTorch 核心架构的关键组件:
Tensor(ATen 库):底层 C++ 实现的多维数组,支持 CPU/CUDA/MPS 等多设备 + 多 dtype。内存由 Storage 管理(引用计数),Tensor 是 Storage 上的 view(stride/offset)。
Autograd(自动微分):动态计算图。每个需要梯度的操作记录到 DAG(grad_fn 链表),反向传播时沿图回溯计算梯度。”Define-by-Run”使得控制流(if/loop)天然支持。
Dispatch 机制:根据 tensor 的 device/dtype/layout 通过调度表(dispatch table)路由到对应 kernel 实现。支持自定义 backend 注册。
torch.compile / TorchDynamo:Python bytecode -> FX Graph -> TorchInductor -> Triton/C++ kernel。Graph capture + JIT 编译优化执行。
分布式(c10d):ProcessGroup 抽象 NCCL/Gloo 等后端,提供 all_reduce/broadcast 等集合通信原语。DDP/FSDP 建立在此之上。
Q: 如何优化CUDA算子?
系统化方法论:
确定瓶颈(Nsight Compute):Roofline Model 判断 compute-bound 还是 memory-bound。大部分 kernel 是 memory-bound。
内存优化(memory-bound 时优先):
- 合并访存:warp 内线程访问连续地址(coalesced,一次事务 128B)
- Shared Memory 缓存:将重复读取的数据搬到片上
- 减少 Bank Conflict:padding 或调整布局
- 向量化加载:float4/int4 一次 128bit
计算优化(compute-bound 时):
- 增加 ILP:每线程处理多元素让 pipeline 填满
- 利用 Tensor Core:WMMA/MMA 指令
- Fast math:rsqrt 代替 sqrt+div
并行度:确保 block 数 >= SM 数 * 2(wave 数充足)。合理 occupancy(不一定越高越好)。
减少开销:kernel fusion(消除中间读写和 launch)、减少
__syncthreads、减少 atomic。