小厂 AI Infra 一面
Q: 如何设计一个分布式推理框架?
分布式推理框架的分层架构:
1 | ┌────────────── API Layer ──────────────────────┐ |
各层关键设计决策:
1. 模型切分策略(决定通信模式):
| 策略 | 适用场景 | 通信模式 | 延迟影响 |
|---|---|---|---|
| TP(张量并行) | 同节点NVLink多卡 | AllReduce每层2次 | 降低延迟 |
| PP(流水线并行) | 跨节点 | 层间Send/Recv | 增加延迟(有bubble) |
| EP(专家并行) | MoE模型 | All-to-All | 取决于负载均衡 |
| TP+PP组合 | 大模型多节点 | 节点内TP+节点间PP | 平衡延迟和扩展性 |
1 | 配置示例(70B模型, 8卡推理): |
2. 调度器设计:
1 | class Scheduler: |
3. KV-Cache管理(PagedAttention):
| 设计点 | 方案 | 原因 |
|---|---|---|
| Block大小 | 16 tokens/block | 平衡碎片和管理开销 |
| 分配策略 | 按需分配(lazy) | 最大化利用率 |
| 回收策略 | 引用计数 | 支持CoW和前缀共享 |
| 跨设备 | TP各rank独立管理各自head的KV | KV按head切分 |
| Swap | GPU↔CPU via PCIe | 抢占时保留计算进度 |
4. 弹性和容错:
- 健康检查: 定期ping每个worker, 超时认为故障
- 故障恢复: 将故障节点的请求重路由到健康节点(重新prefill)
- 弹性扩缩: 根据QPS动态增减推理实例(K8s HPA)
- 请求重试: 客户端超时自动重试到其他实例
Q: 算子优化的一般方法?
算子优化的系统方法论:
1 | Profile(Nsight Compute) |
Memory-bound优化详解:
1 | // 优化1: 合并访问(Coalesced Access) |
Compute-bound优化详解:
1 | // 优化1: Tensor Core (WMMA) |
Occupancy与性能的关系:
1 | Occupancy不是越高越好! |
Q: C++反射机制?
C++缺乏原生反射的问题和解决方案:
1 | // 问题: 无法在运行时根据字符串创建对象 |
1 | // 方案2: 有限RTTI (typeid/dynamic_cast) |
在AI框架中的应用:
- PyTorch C++算子注册:
TORCH_LIBRARY(myops, m) { m.def("my_op", ...); } - TensorRT plugin注册: 工厂模式 + 注册表
- ONNX Runtime: 自定义算子通过字符串名字注册
Q: 工厂模式是什么?
三种工厂模式及其在AI框架中的应用:
1 | // 1. 简单工厂(Static Factory) |
工厂模式的价值:
- 解耦创建和使用(用户不需要知道具体类)
- 方便扩展新类型(不修改已有代码)
- 支持配置驱动(根据config字符串创建对象)
- AI框架中广泛使用:TensorRT plugin、ONNX Runtime、PyTorch算子注册
Q: C++模板编程的作用和特点?
模板的三个层次:
1 | // Level 1: 泛型函数/类(最基本) |
CRTP(Curiously Recurring Template Pattern) — 静态多态:
1 | // 替代虚函数的零开销方案: |
模板在CUDA/HPC中的应用:
- 编译期确定tile大小/block配置
- 零开销的kernel参数化(不同精度/不同策略同一模板)
- cuBLAS/cutlass中大量使用模板元编程生成特化kernel
Q: C++右值引用的应用场景?
四大核心应用场景:
1 | // 1. 移动语义(避免深拷贝) |
Q: C++ static变量的初始化规则?
初始化的三个阶段:
1 | // 阶段1: 零初始化(程序加载时, 所有static变量先清零) |
Static Initialization Order Fiasco:
1 | // file_a.cpp: |
Q: C++内存泄漏的原因与解决?
常见泄漏场景和解决:
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 忘记delete | new后提前return或throw | RAII(unique_ptr) |
| 异常路径泄漏 | catch后未清理 | RAII(析构函数自动清理) |
| 循环引用 | shared_ptr A→B, B→A | weak_ptr打破环 |
| 基类析构非virtual | delete基类指针不调用派生析构 | virtual ~Base() |
| 容器存裸指针 | 容器清空但指针指向的对象没释放 | 存智能指针 |
| 数组delete错误 | new[]配delete(非delete[]) | 使用vector代替裸数组 |
检测工具:
1 | # Valgrind Memcheck(最权威) |
RAII原则的实践:
1 | // Rule of Zero: 如果所有成员都是RAII类型,不需要自定义析构 |
Q: 有符号和无符号字符的最值?
1 | // signed char: 8-bit补码表示 |
在量化中的应用:
- INT8量化: signed范围[-128, 127], 对称量化用[-127, 127]
- UINT8: 某些框架的激活量化用[0, 255]
- 量化计算: INT8×INT8→INT32避免溢出
Q: 链接两个库中有同名函数会怎样?
不同链接类型的行为:
1 | 场景: libA.a 和 libB.a 都定义了 void helper() |
解决方案:
1 | // 1. 命名空间隔离(最推荐) |
C++中的ODR(One Definition Rule):
- 同一符号在整个程序中只能有一个定义
- 违反ODR是未定义行为(不一定报错,可能默默使用错误版本)
- inline函数/模板可以在多个编译单元中有定义(但必须完全相同)