沐曦 AI Infra 实习 一面
Q: CUDA 编程的并行性和并发性有什么区别?
这是理解 GPU 执行模型的重要概念区分:
**并行性(Parallelism)——空间维度的”同时”**:
多个执行单元在同一时刻执行相同(或类似)操作。在 CUDA 中表现为:
- Warp 内 SIMT:同一 warp 的 32 个线程在同一 cycle 执行同一条指令(对不同数据)
- SM 内多 warp:一个 SM 可以同时执行多个 warp 的指令(如 A100 每 SM 有 4 个 warp scheduler)
- 跨 SM 并行:多个 SM 同时执行不同 block(A100 有 108 个 SM)
1 | 时间 → |
**并发性(Concurrency)——时间维度的”重叠”**:
多个任务在时间上重叠执行,但不一定在同一时刻。在 CUDA 中表现为:
多 Stream 并发:
1
2
3
4Stream A: [Kernel 1 计算] [Kernel 3 计算]
Stream B: [H2D 传输] [Kernel 2 计算]
Stream C: [D2H 传输]
↑ 不同硬件单元同时工作- Copy Engine(DMA)和 Compute Engine(SM)是独立硬件
- 可以传输和计算同时进行(需要不同 stream + pinned memory)
SM 上多 Block 并发:
- 单个 SM 上可能驻留 2-16 个 block(取决于资源)
- 同一时刻只有部分 warp 在执行,其余在等待(memory access、sync 等)
- 调度器在 ready warp 间切换 → 并发执行
Graph-level 并发:
- CUDA Graph 中无依赖的 kernel 可以并发(如果 SM 资源足够)
总结对比:
| 维度 | 并行性 | 并发性 |
|---|---|---|
| 本质 | 多个执行单元同时做 | 多个任务时间上重叠 |
| CUDA 体现 | warp 内 32 线程同步执行 | 多 stream 的 kernel/传输重叠 |
| 硬件需求 | 足够的计算单元 | 独立的硬件功能单元 |
| 目的 | 加速计算(数据并行) | 隐藏延迟、提高利用率 |
| 类比 | 32 人同时搬砖 | 一边搬砖一边等水泥搅拌 |
Q: C++ 的三大特性?
封装(Encapsulation):
将数据(成员变量)和操作数据的方法(成员函数)绑定在类中,通过访问控制隐藏内部实现:
1 | class KVCache { |
核心价值:
- 降低耦合(修改内部实现不影响外部使用者)
- 保护数据完整性(外部无法直接修改 private 数据)
- 明确的接口契约(public 方法定义了”能做什么”)
继承(Inheritance):
子类继承父类的属性和方法,实现代码复用和层次化设计:
1 | class BaseAttention { |
继承方式:public(最常用)、protected、private。
多态(Polymorphism):
同一接口、不同实现。分为编译时多态和运行时多态:
1 | // 运行时多态(虚函数 + 动态绑定) |
虚函数表(vtable)机制:
- 每个有虚函数的类维护一个虚函数表(函数指针数组)
- 每个对象有一个 vptr 指向所属类的 vtable
- 调用虚函数时:
obj->vptr->vtable[func_index](args) - 开销:一次额外的间接寻址(~几 ns),在 GPU kernel 中应避免
Q: map 和 unordered_map 的底层实现区别?
| 维度 | std::map | std::unordered_map |
|---|---|---|
| 底层结构 | 红黑树(自平衡 BST) | 哈希表(开链法:bucket 数组 + 链表/红黑树) |
| 有序性 | key 有序(按 operator< 排列) |
无序(按 hash 值分桶) |
| 查找 | O(log n) 保证 | 平均 O(1),最坏 O(n)(hash 冲突严重时) |
| 插入 | O(log n) + 可能旋转 | 平均 O(1),最坏 O(n)(rehash 时 O(n)) |
| 删除 | O(log n) | 平均 O(1) |
| 内存布局 | 节点分散分配(指针连接) | 桶数组 + 节点链表 |
| 迭代器稳定性 | 插入/删除不影响其他迭代器 | rehash 会使所有迭代器失效 |
| 空间开销 | 每节点 3 指针 + color | 桶数组 + 每节点 1-2 指针 |
| Cache 友好度 | 差(节点分散在堆上) | 中(桶数组连续,链表仍分散) |
选择指南:
1 | 需要有序遍历(如 range query) → std::map |
unordered_map 的 rehash:
- 当 load_factor = size / bucket_count > max_load_factor(默认 1.0)时触发
- Rehash:分配更大的桶数组(通常 2x),所有元素重新 hash 分配
- 时间复杂度 O(n),但均摊后仍是 O(1)
Q: new、malloc 和智能指针的区别?底层原理?
三者对比:
| 维度 | malloc | new | 智能指针 |
|---|---|---|---|
| 语言 | C | C++ | C++ (C++11+) |
| 分配 | 原始字节 | 对象(分配+构造) | 对象 + 自动管理 |
| 释放 | free() | delete | 自动(析构时) |
| 失败行为 | 返回 NULL | 抛 std::bad_alloc | 同 new |
| 类型安全 | void*(需强转) | 返回类型化指针 | 类型化包装 |
malloc 底层原理:
1 | 用户调用 malloc(size): |
new 的执行流程:
1 | MyClass* p = new MyClass(args); |
智能指针详解:
1 | // unique_ptr: 独占所有权,零开销 |
make_shared 的优势:
1 | // 方式 1: 两次分配 |
Q: 介绍一下 C++ Lambda 表达式?
Lambda 是 C++11 引入的匿名函数对象,本质是编译器生成的匿名类的实例。
完整语法:
1 | [capture](parameters) mutable -> return_type { body } |
捕获方式详解:
1 | int x = 10; |
Lambda 的编译器实现:
1 | // 源码: |
mutable 关键字:
1 | int count = 0; |
常见用途:
1 | // 1. STL 算法 |
注意事项:
- 悬垂引用:引用捕获时,如果 lambda 的生命周期超过被捕获变量 → undefined behavior
- 大对象捕获:值捕获大对象(如 vector)会拷贝 → 用
[v = std::move(v)]或引用 - this 捕获:
[this]捕获 this 指针,注意对象生命周期
Q: 手撕 Transformer 结构(PyTorch)?
(编程题)
Q: 手撕:两数之和?
(编程题)