小米 AI Infra 实习 一二面
Q: Python深拷贝和浅拷贝的区别?
浅拷贝(copy.copy)只复制对象本身(最外层容器),不复制其引用的子对象,新旧对象共享内部可变对象;深拷贝(copy.deepcopy)递归复制所有层级的对象,新旧对象完全独立互不影响。
核心机制对比:
| 特性 | 浅拷贝 copy.copy | 深拷贝 copy.deepcopy |
|---|---|---|
| 复制层级 | 仅最外层 | 递归所有层级 |
| 内部可变对象 | 共享引用 | 独立副本 |
| 性能开销 | 低 | 高(需遍历对象图) |
| 循环引用处理 | 不涉及 | 自动处理(memo字典) |
关键行为细节:
- 对于不可变对象(如int、str、tuple),浅拷贝通常不会创建新对象,而是直接复用引用(因为不可变,共享无害)。但如果tuple内部包含可变对象,浅拷贝后修改内部可变对象仍会相互影响。
- 列表的切片操作
a[:]、list(a)、dict.copy()都是浅拷贝。 deepcopy内部维护一个 memo 字典追踪已复制对象,避免循环引用导致无限递归。自定义类可通过实现__copy__和__deepcopy__方法控制拷贝行为。
1 | import copy |
常见陷阱:NumPy数组的切片是视图(view),不是拷贝;需要 .copy() 方法获得独立副本。
Q: C++的三种智能指针及其用法?
C++11引入的三种智能指针通过RAII机制自动管理堆内存生命周期,消除手动new/delete的内存泄漏风险。
1. unique_ptr —— 独占所有权
独占所有权语义,同一时刻只有一个unique_ptr指向该对象。不可复制,只能通过 std::move 转移所有权。析构时自动释放资源。
- 实现原理:内部仅存储一个裸指针(+可选删除器),禁用拷贝构造和拷贝赋值运算符。
- 零开销抽象:sizeof(unique_ptr) == sizeof(raw pointer)(无自定义删除器时),无引用计数开销。
- 适用场景:工厂函数返回值、独占资源(文件句柄、GPU buffer)。
2. shared_ptr —— 共享所有权
多个shared_ptr可指向同一对象,内部通过引用计数管理生命周期。最后一个shared_ptr销毁时自动释放资源。
- 控制块结构:堆上分配的控制块包含——强引用计数(atomic)、弱引用计数(atomic)、删除器(deleter)、分配器(allocator)。
- 线程安全性:引用计数的增减是原子操作(线程安全),但所指向对象的读写不是线程安全的。
make_shared优势:一次内存分配同时创建对象和控制块(减少一次heap allocation),提高cache友好性,避免裸new异常安全问题。- 开销:每个shared_ptr 16字节(指针+控制块指针),控制块约32-40字节。
3. weak_ptr —— 弱引用观察者
不增加强引用计数,用于解决shared_ptr循环引用问题。使用前需 lock() 提升为shared_ptr,检查对象是否仍然存活。
- 典型场景:观察者模式中观察者持有目标的weak_ptr、缓存系统中cache条目、树结构中子节点指向父节点。
1 | // make_shared vs new的区别 |
常见陷阱:避免从this创建shared_ptr(应继承 enable_shared_from_this);避免循环引用(A持有B的shared_ptr,B持有A的shared_ptr)。
Q: 写时拷贝(COW)原理?
写时拷贝(Copy-On-Write)是一种延迟复制的优化策略,核心思想:只有在真正需要修改时才付出拷贝代价。
实现机制:
- 多个对象初始共享同一块内存数据。
- 通过引用计数追踪共享者数量。
- 读操作直接访问共享内存,无任何开销。
- 写操作时检查引用计数——若引用计数>1,先复制一份独立副本,再在副本上修改;若引用计数==1,说明独占,直接修改。
典型应用:
- **Linux fork()**:子进程创建时不复制父进程的物理页面,只复制页表(标记为只读)。当任一进程写入某页时触发page fault,内核此时才复制该页。对于大量fork()+exec()的场景极大减少了无用拷贝。
- 早期 std::string(GCC < 5):多个string对象共享同一缓冲区,修改时才复制。但在多线程环境下引用计数需原子操作,性能反而下降,因此C++11后std::string标准禁止COW实现。
- QEMU/虚拟化快照:qcow2镜像格式基于COW实现快照链。
性能权衡:COW在读多写少的场景收益大;如果写操作频繁,COW的引用计数检查和潜在的延迟拷贝反而增加开销。此外COW在多线程环境下需要原子操作保护引用计数,可能产生cache line争用。
Q: 零拷贝原理?
零拷贝(Zero-Copy)指数据传输过程中避免CPU在内核空间和用户空间之间进行冗余数据拷贝,从而降低CPU开销和内存带宽消耗。
传统I/O路径(4次拷贝+4次上下文切换):
1 | 磁盘 → DMA → 内核页缓存 → CPU → 用户缓冲区 → CPU → Socket缓冲区 → DMA → 网卡 |
零拷贝技术演进:
| 技术 | 拷贝次数 | CPU参与拷贝 | 适用场景 |
|---|---|---|---|
| 传统read+write | 4次 | 2次 | - |
| mmap+write | 3次 | 1次 | 需要应用层处理数据 |
| sendfile | 3次 | 1次 | 静态文件传输 |
| sendfile+SG-DMA | 2次 | 0次 | 网卡支持scatter-gather |
| splice | 2次 | 0次 | 管道中转 |
各技术详解:
- mmap:将内核页缓存映射到用户空间地址,应用可直接读取(少一次内核→用户拷贝)。但仍需一次从页缓存到socket缓冲区的CPU拷贝。
- sendfile:数据直接在内核空间从页缓存传到socket缓冲区,不经过用户空间。减少两次上下文切换。
- sendfile + DMA scatter-gather:网卡支持scatter-gather DMA时,只需将文件描述符(地址+长度)传给socket缓冲区,DMA引擎直接从页缓存搬数据到网卡,实现真正的零CPU拷贝。
- splice:通过内核管道作为中介,利用页面引用计数在两个fd间移动数据,无实际数据拷贝。
实际应用:Kafka使用sendfile实现高吞吐消息投递;Nginx的sendfile指令;RDMA在GPU通信中实现真正的零拷贝(绕过CPU和OS内核)。
Q: 大模型分布式训练的流程和并行策略如何选择?
分布式训练完整流程:
- 数据准备:数据分片、tokenization、构建DataLoader(每卡加载不同数据分片)。
- 模型初始化:加载预训练权重、按并行策略分片模型(TP切权重、PP分层、ZeRO分片参数)。
- 前向计算:各卡并行执行前向,TP需AllReduce/AllGather通信,PP需跨stage传输激活。
- 梯度计算:反向传播计算局部梯度。
- 梯度同步:数据并行维度做AllReduce聚合梯度(可与反向重叠)。
- 参数更新:优化器更新参数(ZeRO下各卡更新自己负责的分片)。
并行策略选择决策树:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 模型放得进单卡 | 纯数据并行(DDP) | 最简单、扩展性最好、通信开销最小 |
| 单层参数超单卡显存 | 张量并行(TP) | 切分权重矩阵到多卡 |
| 模型层数多、跨节点 | 流水线并行(PP) | 通信量小(仅传激活),适合高延迟网络 |
| 超长序列(>128K) | 序列并行(SP/CP) | 切分序列维度减少激活内存 |
| 训练规模极大 | 3D并行组合 | TP节点内 + PP跨节点 + DP扩batch |
实践中的选择逻辑:
- 节点内(NVLink 600GB/s):TP = 4/8,利用高带宽做频繁通信。
- 节点间(IB 400Gb/s ~ 50GB/s):PP和DP,通信量相对小。
- 典型配置举例:训练Llama-70B,TP=8(节点内),PP=4(4个节点),DP=4(32卡总共),global batch通过梯度累积扩大。
Q: 各种并行策略介绍?
1. 数据并行(DP/DDP/ZeRO)
每卡持有完整模型副本,将训练数据的不同mini-batch分到各卡。前向和反向独立计算,梯度通过AllReduce同步后统一更新。通信量为 2 x 模型参数量(reduce-scatter + all-gather)。
- DDP相比DP的改进:多进程(避免GIL)、梯度桶通信与计算overlap。
- ZeRO-1/2/3逐步将优化器状态/梯度/参数分片,显存从16Φ降至16Φ/N。
2. 张量并行(Tensor Parallelism, TP)
将单个算子的权重矩阵按行或列切分到多卡,每卡计算部分结果后通过AllReduce或AllGather通信汇总。通信频率极高(每层2次),因此必须使用节点内NVLink高带宽互联。
- 通信量:每层前向一次AllReduce(2 x hidden_size x seq_len x batch / TP),反向同等。
3. 流水线并行(Pipeline Parallelism, PP)
将模型按层切分为多个stage,各stage分布在不同设备上流水执行。每个micro-batch依次经过各stage。
- 调度策略:GPipe(先全前向再全反向,bubble大)、1F1B(交替执行减少bubble到 (pp-1)/micro_batches)。
- bubble占比 ≈ (PP-1) / (PP-1 + num_microbatches),micro-batch越多bubble越小。
4. 序列并行(Sequence Parallelism, SP)
将序列维度切分到多卡,主要处理LayerNorm和Dropout等非张量并行区域。Megatron-SP将AllReduce拆为Reduce-Scatter + AllGather,每卡只处理1/TP的序列片段,激活内存降为1/TP。
5. 专家并行(Expert Parallelism, EP)
MoE模型中将不同expert分配到不同卡。Router决定token路由,通过All-to-All通信将token发送到对应expert所在卡。关键挑战是负载均衡。
Q: TP为什么有按行/列两种切分方式,分别对应什么?
在Transformer的MLP层中,存在两个线性变换:第一层升维(h→4h),第二层降维(4h→h)。两种切分方式配合使用能最小化通信次数。
列并行(Column Parallel)—— 应用于第一个线性层:
对权重矩阵A按列切分:A = [A1, A2](每卡持有一半列)。计算 Y = XA 时:
- 每卡计算 Yi = X × Ai,得到输出的不同列。
- 输出在列维度上自然分割,无需通信即可直接作为下一层(第二个线性层)的输入。
- 配合GeLU激活:GeLU(XA1) 与 GeLU(X) × A1 不等价,因此GeLU必须在分割后的输出上执行(这也是列切分放在第一层的原因)。
行并行(Row Parallel)—— 应用于第二个线性层:
对权重矩阵B按行切分:B = [B1; B2]。输入已经在各卡上按列分割:
- 每卡计算 Zi = Yi × Bi,得到部分结果(维度相同但只是部分和)。
- 需要一次 AllReduce求和 得到最终输出 Z = Z1 + Z2。
通信分析:
- MLP前向:1次AllReduce。反向:1次AllReduce。
- Attention层类似:QKV投影列并行,输出投影行并行,前向1次AllReduce。
- 整体每个Transformer层:前向2次AllReduce(MLP + Attention),反向2次。
Q: Megatron-SP(序列并行)介绍?
Megatron-SP是在张量并行基础上的进一步优化,核心目标是减少非TP区域的激活内存占用。
背景问题:在标准TP中,LayerNorm和Dropout操作需要完整的激活张量(因为它们不在TP切分的维度上操作)。这意味着即使使用TP=8,这些操作的激活仍占完整大小,造成内存浪费。
Megatron-SP解决方案:
- 将LayerNorm和Dropout的输入在序列维度上切分到TP组内的各GPU上。
- 每卡只处理 1/TP 的序列长度对应的LayerNorm/Dropout操作。
- 进入TP区域(如QKV投影)前,通过AllGather收集完整激活。
- TP区域输出后,通过Reduce-Scatter同时完成规约和分散。
关键洞察:原来TP中的AllReduce = Reduce-Scatter + AllGather。Megatron-SP只是将这两步分开使用——AllGather在进入TP区域时执行,Reduce-Scatter在离开TP区域时执行。总通信量不变,但激活内存在非TP区域降为1/TP。
具体效果:
- 激活内存节省:LayerNorm和Dropout的激活占比约30-40%,这部分降为1/TP。
- 对于TP=8,非TP区域的激活内存减少8倍。
- 通信量与标准TP完全相同(只是拆分了AllReduce的时机)。
Q: Transformer架构中有哪些层和算子?
Transformer由多个相同结构的层堆叠而成,每层包含以下核心组件和对应的计算算子:
Multi-Head Attention子层:
- QKV线性投影:3个GEMM操作(或融合为1个),将hidden_state投影为Q/K/V
- 注意力分数计算:矩阵乘 QK^T,计算量 O(n^2 x d)
- Scale:逐元素除以 sqrt(d_k)
- Causal Mask:上三角mask填负无穷(Decoder)
- Softmax:指数+归一化,数值稳定需先减max
- Attention加权:矩阵乘 softmax_output x V
- 输出投影:1个GEMM,将多头拼接结果映射回hidden_size
Feed-Forward Network(MLP/FFN)子层:
- Gate投影 + Up投影:2个GEMM(SwiGLU架构),将hidden_size映射到intermediate_size
- 激活函数:SiLU/GeLU(逐元素非线性)
- Down投影:1个GEMM,将intermediate_size映射回hidden_size
归一化与残差:
- RMSNorm/LayerNorm:逐元素操作(求均值/方差、归一化、缩放)
- 残差连接:逐元素加法
输入输出层:
- Token Embedding:查表操作(大词表时可视为GEMM)
- 位置编码:RoPE(旋转位置编码,在QK上应用旋转矩阵)
- LM Head:GEMM + Softmax(或直接取argmax)
计算量分布(典型LLM如Llama-70B):GEMM(MLP+Attention投影)占90%+,Softmax/Norm/激活占<10%。因此GEMM优化是核心。
Q: Encoder和Decoder的介绍与特点?
Encoder(双向注意力):
- 注意力机制:每个token可以看到输入序列中的所有token(full attention),没有mask。
- 适合任务:需要全局理解的任务——文本分类、命名实体识别(NER)、句子相似度、特征提取。
- 代表模型:BERT、RoBERTa、DeBERTa。
- 训练方式:Masked Language Modeling(随机mask 15% token预测)。
- 特点:不能直接做生成,但理解能力强。
Decoder(因果注意力):
- 注意力机制:因果mask(causal mask),每个token只能attend到自身及之前的token。训练时通过下三角mask实现并行计算。
- 适合任务:文本生成、对话、代码生成——所有自回归生成任务。
- 代表模型:GPT系列、Llama、Qwen、DeepSeek。
- 训练方式:Next Token Prediction(每个位置预测下一个token)。
- 特点:天然支持生成,通过in-context learning也能做理解任务。
Encoder-Decoder(交叉注意力):
- 结构:Encoder编码输入得到表示,Decoder通过cross-attention关注encoder输出并生成。
- 适合任务:输入和输出结构不同的seq2seq任务——翻译、摘要、ASR。
- 代表模型:T5、BART、Whisper。
- 特点:encoder处理输入不需要因果约束,decoder生成时有因果mask。
为什么Decoder-only成为主流:训练效率高(每个token都贡献loss)、结构简单(易于并行和优化)、scaling law表现最好、统一了理解和生成(通过prompt工程)。
Q: FlashAttention介绍?
FlashAttention是一种IO-aware的精确注意力算法,核心创新在于通过分块计算(tiling)和在线统计量更新(online softmax)避免在HBM中显式存储 N x N 的注意力矩阵。
为什么需要FlashAttention:
- 标准Attention的内存复杂度O(N^2),当序列长度N=8192时,注意力矩阵占64M个float16 = 128MB/head。
- GPU的HBM带宽(如A100 2TB/s)远慢于SRAM(19TB/s),大量中间矩阵的HBM读写成为瓶颈。
- 实测标准Attention中,HBM访问时间占总时间的60-80%。
核心算法:
- 将Q按行分块(block size Br),K和V按行分块(block size Bc)。典型值:Br=Bc=128。
- 外层循环遍历Q的每个block。
- 内层循环遍历K、V的每个block:
- 从HBM加载Qi、Kj、Vj到SRAM(片上共享内存,如192KB/SM)。
- 在SRAM中计算局部注意力分数 Sij = Qi x Kj^T。
- 使用online softmax增量更新全局最大值和归一化因子。
- 更新输出结果(无需等所有block计算完)。
- 最终输出直接写回HBM,中间不存储N x N矩阵。
效果与性能数据:
- 内存复杂度:O(N^2) → O(N),节省的内存可用于增大batch或加长序列。
- HBM访问量:从O(N^2 x d)降至O(N^2 x d^2 / SRAM_size)。
- 实测加速:比PyTorch标准Attention快2-4倍(A100上),训练端到端加速15-20%。
- 精度:数学等价(不是近似),结果bit-wise相同(除浮点重排序误差)。
V2改进:减少非matmul FLOPs、改进并行度(外循环在Q block上)、优化warp分工(4个warp各处理不同KV block而非reduce),速度比V1提升约2倍。
Q: Online Softmax介绍?
Online Softmax允许在不知道全部元素的情况下增量计算softmax,是FlashAttention实现分块计算的数学基础。
传统Softmax的问题(需两次完整遍历):
- 第一遍:求全局max(数值稳定性):m = max(x1, x2, …, xN)
- 第二遍:求归一化因子和输出:d = sum(exp(xi - m)), softmax(xi) = exp(xi - m) / d
这要求所有元素同时可用,不适合分块处理。
Online Softmax算法:
在一次遍历中维护两个统计量:
m:当前已见元素的最大值d:当前累积的 sum(exp(xi - m))
当新block(含新的一组元素)到来时,更新规则:
1 | m_new = max(m_old, m_block) // 更新全局最大值 |
对于FlashAttention中输出O的更新:
1 | O_new = O_old × (d_old × exp(m_old - m_new) / d_new) // 修正旧输出的权重 |
为什么有效:
- 每个block处理后,O已经是基于当前所有已见block的正确softmax加权结果。
- 只需存储O、m、d这三个统计量(O(N)空间),无需存储完整的N x N注意力矩阵。
- 数学上完全等价于标准softmax(不是近似),只是改变了计算顺序。
Q: CUDA中的Block是软件还是硬件概念?
Block(Thread Block)是软件(编程模型)概念,是程序员定义的线程组织单位,用于表达哪些线程需要协作和共享数据。
软件层面:
- 程序员在kernel launch时指定block大小(如
<<<grid, block>>>)。 - Block内线程可以通过共享内存通信、通过
__syncthreads()同步。 - Block大小通常为32的倍数(如128、256),最大不超过1024个线程。
硬件映射:
- 一个Block被调度到一个SM(Streaming Multiprocessor)上执行,整个生命周期不会迁移。
- Block内线程共享该SM的shared memory资源。
- 一个SM可同时驻留多个Block(受寄存器文件、共享内存、warp槽位限制)。
- Block内的线程进一步被组织为Warp(32个线程),Warp是硬件的实际执行和调度单位。
层次关系:
1 | Grid(软件,一个kernel的所有线程) |
设计哲学:这种软硬件解耦使得同一CUDA程序可以在不同代GPU(SM数量不同)上运行——运行时由硬件调度器决定Block到SM的映射,程序员无需关心具体硬件配置。
Q: CUDA有哪些优化方法?
CUDA优化是一个系统工程,需要根据Profiling结果确定瓶颈类型(memory-bound vs compute-bound)后针对性优化。
1. 访存优化(解决memory-bound瓶颈):
- 合并全局内存访问(Coalesced Access):确保warp内32个线程访问连续128字节地址,一次事务完成。错位/跳跃访问会拆分为多次事务,带宽浪费可达32倍。
- 共享内存缓存:将频繁复用的数据从HBM(
2TB/s)加载到shared memory(19TB/s/SM),典型如GEMM的tile。 - 向量化读写(float4/int4):一条指令搬运128位,减少指令数和事务数。要求16字节对齐。
- 利用只读缓存:
__ldg()或const __restrict__提示走texture/L1 readonly路径。
2. 计算优化(解决compute-bound瓶颈):
- 减少Warp Divergence:将相似条件的线程映射到同一warp,或用无分支代码(bitwise ops)替代if-else。
- 利用Tensor Core:FP16/BF16/INT8矩阵运算,吞吐比CUDA Core高8-16倍。通过WMMA API或cuBLAS自动使用。
- 快速数学函数:
__expf()、__sinf()等,牺牲少量精度换取数倍速度。 - 循环展开:
#pragma unroll减少循环控制开销,增加ILP。
3. 并行度优化:
- 提高Occupancy:平衡寄存器/共享内存使用,让更多warp同时驻留。用
__launch_bounds__限制寄存器。 - 合理设置Block/Grid大小:Block为32倍数(满warp),Grid block数为SM数的整数倍(避免tail effect)。
4. 延迟隐藏:
- Double Buffering/Prefetch:计算buffer A的同时异步加载buffer B(cp.async),交替使用。
- 增加活跃Warp数:当一个warp stall在访存时,调度器切换到另一个就绪warp。
5. 算法/系统级优化:
- 算子融合:多个kernel合并减少launch开销和中间tensor的HBM读写。
- 异步执行:多Stream实现计算与通信overlap。
- 持久化Kernel:kernel不退出,循环处理任务,减少launch开销。
Q: 访存优化方法有哪些?
访存优化是CUDA性能优化中最重要的方向,因为大部分kernel都是memory-bound的。
1. Coalesced Access(合并访问)
GPU内存系统以32字节/128字节为粒度的事务(transaction)传输数据。Warp内32个线程同时发起的访存请求,如果地址连续且对齐,可以合并为最少的事务:
- 理想情况:32个线程访问连续128字节,仅1次128B事务。
- 最差情况:32个线程各访问不同cache line,需要32次事务,带宽利用率仅1/32。
- 实践:让thread_id对应连续的内存地址(如
data[threadIdx.x]而非data[threadIdx.x * stride])。
2. 共享内存(Shared Memory)
片上SRAM(如A100每SM 192KB),带宽约19TB/s,延迟约20-30 cycles(vs HBM的~400 cycles):
- 使用模式:线程协作加载全局内存tile到shared memory →
__syncthreads()→ 多次重复访问shared memory中的数据。 - Bank Conflict:32个bank,每bank宽4字节。同一warp中的不同线程访问同一bank的不同地址会串行化。解决:padding(在shared memory数组每行末尾加1个元素)或调整访问模式。
3. 向量化读写(Vectorized Load/Store)
使用 float4(128位)一次性读写4个float值:
- 减少load/store指令数量(4倍)。
- 减少内存事务数。
- 前提条件:数据起始地址必须16字节对齐。
4. 数据预取(Prefetch/Double Buffering)
在计算当前数据的同时,异步加载下一批数据:
1 | // 使用cp.async实现异步拷贝 |
5. 只读缓存路径
通过 __ldg() 或声明 const __restrict__ 指针,提示编译器使用L1 read-only cache路径,避免L1/shared memory的容量争用。
6. 减少冗余访存
算子融合避免中间结果写回HBM再读取。例如将LayerNorm的reduce、normalize、scale三步融合为一个kernel,中间结果保留在寄存器中。
7. 寄存器复用
让数据尽量停留在寄存器中:每个线程处理多个元素(增加ILP),循环展开后中间值不溢出到local memory。
Q: 计算和访存如何overlap?
计算和访存overlap的核心思想是让GPU的不同硬件单元同时工作——在计算单元执行运算的同时,内存子系统(DMA/TMA)执行数据搬运。
Warp级别天然延迟隐藏(TLP):
GPU通过大量Warp切换天然隐藏访存延迟——当一个Warp的load指令发出后进入等待状态,调度器立即切换到另一个数据已就绪的Warp执行计算。这是GPU最基本的延迟隐藏机制,要求有足够多的活跃Warp(即足够的Occupancy)。
Double Buffering(显式软件流水线):
1 | 阶段0: Load A0 到 buffer_A |
实现要点:
- 分配两份shared memory buffer(或寄存器buffer)。
- 使用
cp.async(Ampere+)或TMA(Hopper)指令发起异步拷贝,数据搬运由DMA硬件执行,不占用CUDA Core。 - 通过
cp.async.wait_group或mbarrier等待数据就绪。 - 交替使用两个buffer,实现计算和搬运的完美重叠。
Hopper架构的Warp Specialization:
更进一步,将Block内的Warp分为Producer(专职搬运)和Consumer(专职计算):
- Producer Warp使用TMA硬件单元持续搬运数据到shared memory。
- Consumer Warp从shared memory读取数据执行GEMM(WGMMA指令)。
- 通过异步mbarrier协调,实现零等待流水线。
Stream级别的Compute-Communication Overlap:
在分布式训练中,使用不同的CUDA Stream让AllReduce通信和下一层的前向计算并行执行:
1 | # DDP中的梯度桶通信与反向计算overlap |
Q: GPU中L1和L2缓存的区别?
| 特性 | L1 Cache | L2 Cache |
|---|---|---|
| 作用域 | 每个SM私有 | 全部SM共享 |
| 容量(A100) | 192KB/SM(与Shared Memory共用) | 40MB(全局) |
| 容量(H100) | 228KB/SM | 50MB |
| 访问延迟 | ~30 cycles | ~200 cycles |
| 带宽 | ~19 TB/s(每SM) | ~5 TB/s(总) |
| 管理方式 | 硬件自动 + 可配置与shared memory的比例 | 纯硬件自动管理 |
| 缓存行大小 | 128 bytes | 128 bytes |
| 功能定位 | 缓存当前SM的工作集 | 跨SM数据共享和全局访问加速 |
L1 Cache详解:
- 与Shared Memory共享同一物理SRAM(如A100共192KB),可通过
cudaFuncSetAttribute配置比例。 - L1命中时无需访问L2和HBM,是全局内存访问的第一级加速。
- 对于只读数据(
__ldg),L1有单独的只读缓存路径。
L2 Cache详解:
- 所有SM共用,当多个SM访问相同数据时L2提供缓存(如reduction操作的中间结果)。
- Ampere+架构支持L2 cache residency control(
cudaAccessPolicyWindow),可将热数据持久化在L2中。 - L2是HBM的前端缓存,HBM bandwidth(2TB/s)是数据从DRAM到L2的带宽。
数据流路径:
1 | Register → L1/Shared Memory(~30 cycles)→ L2 Cache(~200 cycles)→ HBM(~400 cycles) |
Q: 共享内存和L1缓存的区别?
| 特性 | Shared Memory | L1 Cache |
|---|---|---|
| 管理方式 | 程序员显式管理(__shared__声明) |
硬件自动管理,对程序透明 |
| 生命周期 | 程序员控制(block存在期间有效) | 缓存替换策略决定(LRU) |
| 可靠性 | 数据一定在(程序员保证) | 可能miss(需重新从下级加载) |
| 访问模式 | 32个bank并行访问 | Tag查找 + Hit/Miss判断 |
| 特殊问题 | Bank Conflict(同bank不同地址串行) | Cache Miss导致stall |
| 编程复杂度 | 需要手动load/sync/访问 | 自动,无需额外代码 |
| 适用场景 | 明确知道数据复用pattern | 不确定访问pattern时的通用加速 |
物理实现:
- 两者共享同一块物理SRAM(如A100每SM 192KB),通过硬件配置决定各占多少。
- 可选配置:prefer shared(164KB shared + 28KB L1)或 prefer L1(28KB shared + 164KB L1),默认平分。
- Hopper架构(H100)最大支持228KB shared memory per SM。
Bank Conflict详解:
- Shared memory被分为32个bank,每bank宽4字节,地址按
bank_id = (address / 4) % 32映射。 - 同一warp中如果两个线程访问同一bank的不同行地址,会产生conflict导致串行化。
- 特殊情况:所有线程访问同一地址是broadcast(无conflict)。
- 解决方案:padding(每行末尾加一个元素偏移bank映射)。
选择建议:
- 数据复用次数多、访问pattern规律 → 用Shared Memory。
- 临时数据、不确定是否复用 → 依赖L1 Cache。
- GEMM中的tile → Shared Memory;逐元素kernel → L1自动缓存。
Q: 为什么需要共享智能指针(shared_ptr)?
当多个对象需要共同拥有某个资源的所有权,且无法确定谁最后使用完该资源时,unique_ptr的独占语义不适用,这正是shared_ptr的应用场景。
典型使用场景:
- 观察者模式:多个观察者引用同一个Subject对象,Subject应在所有观察者都不再需要时才析构。
- 图/DAG结构:多个节点指向同一子节点(如AST中多个表达式共享同一子表达式节点)。
- 缓存系统:缓存池和使用者同时持有引用,使用者释放后若缓存也不再持有则真正释放。
- 异步回调/Future:多个callback或continuation共享同一个上下文对象,最后一个完成时释放。
- 多线程共享数据:多个线程共同读取的只读数据结构。
为什么不用裸指针:
- 裸指针无法表达所有权语义,不知道谁负责释放。
- 手动管理多所有者场景极易出bug(double free或memory leak)。
shared_ptr的代价:
- 控制块堆分配开销(
make_shared优化为与对象同一次分配)。 - 引用计数的原子操作开销(每次拷贝/析构涉及atomic increment/decrement,在高争用下可能成为瓶颈)。
- 对象大小增加(16字节:对象指针 + 控制块指针)。
设计原则:优先使用unique_ptr(零开销),只在确实需要共享所有权时才使用shared_ptr。用weak_ptr打破循环引用。
Q: AllReduce介绍及实现方式?
AllReduce是分布式训练中最核心的集合通信原语,功能是将所有节点的数据做规约(如求和)后将完整结果广播到所有节点。
语义:假设N个节点各持有向量Vi,AllReduce后每个节点都得到 V1 + V2 + … + VN。
实现方式对比:
| 算法 | 通信量/节点 | 延迟(步数) | 适用场景 |
|---|---|---|---|
| Ring AllReduce | 2(N-1)/N × M ≈ 2M | 2(N-1) | 大消息、节点数适中 |
| Tree AllReduce | 2M | 2×log(N) | 小消息、节点数多 |
| Recursive Halving-Doubling | 2M | 2×log(N) | 小消息、2的幂次节点 |
| Chunked Pipeline | ~2M | O(N) + O(chunks) | 超大消息 |
1. Ring AllReduce(NCCL大消息默认):
N个节点组成逻辑环,数据切分为N个chunk,分两阶段:
- Reduce-Scatter阶段(N-1步):每步每节点向下一个发送一个chunk,接收并累加,最终每节点持有1/N数据的完整规约结果。
- AllGather阶段(N-1步):每步发送已完成的chunk,N-1步后所有节点持有完整结果。
2. Tree AllReduce(NCCL小消息默认):
构建树形拓扑,先Reduce到根(logN步),再Broadcast从根到叶(logN步)。延迟优势在节点数多时显著,但根节点成为带宽瓶颈。
3. NCCL实际行为:
NCCL根据消息大小和网络拓扑自动选择算法:
- 小消息(<256KB):Tree算法(延迟优先)。
- 大消息(>256KB):Ring算法(带宽优先)。
- NVLink拓扑:使用NVLink的全连接特性优化,不严格遵循ring。
- 跨节点:利用IB RDMA的特性做multi-rail优化。
Q: Ring AllReduce的通信容量分析?
设定:N个节点,每个节点持有大小为M的数据需要AllReduce。
Ring将数据均分为N个chunk,每个chunk大小 M/N:
Reduce-Scatter阶段:
- 共执行 N-1 步。
- 每步每节点向下一个邻居发送 M/N 数据,同时从上一个邻居接收 M/N 数据并累加。
- 单节点总发送量 = (N-1) × M/N。
- 结束时每节点持有 1/N 数据的完整规约结果。
AllGather阶段:
- 同样 N-1 步。
- 每步每节点发送 M/N 已规约完成的chunk给下一个邻居。
- 单节点总发送量 = (N-1) × M/N。
- 结束时每节点持有完整的AllReduce结果。
总通信量分析:
- 单节点总发送量 = 2 × (N-1)/N × M
- 当N较大时趋近 2M(与节点数无关),这是带宽最优的。
- 理论下界证明:AllReduce的通信量下界就是2M,Ring达到了这个最优。
延迟分析:
- 总步数 = 2(N-1)
- 每步延迟 = alpha(启动延迟)+ M/(N × BW)(传输延迟)
- 总延迟 = 2(N-1) × [alpha + M/(N × BW)]
- 延迟随N线性增长——这是Ring的主要缺点,节点数很多时延迟累积严重。
数值示例(A100集群,8节点Ring,梯度1GB):
- 每节点发送 ≈ 2GB,带宽200GB/s(IB HDR),传输时间 ≈ 10ms。
- 14步 × alpha ≈ 14 × 5us = 70us。
- 总时间 ≈ 10ms + 70us ≈ 10ms(大消息下alpha可忽略)。
Q: Tree AllReduce相比Ring有什么优点?
**核心优势:延迟 O(log N) vs Ring的 O(N)**。
在集合通信的延迟模型 T = alpha × steps + M/BW × steps 中:
- Ring步数 = 2(N-1),延迟与节点数线性增长。
- Tree步数 = 2×log2(N),延迟与节点数对数增长。
当alpha主导(小消息场景)时:
假设N=256节点:
- Ring延迟 ≈ 2×255×alpha = 510×alpha
- Tree延迟 ≈ 2×8×alpha = 16×alpha
- Tree快 32倍!
Tree的适用场景:
- 小消息:梯度较小时启动延迟alpha占主导,Tree的logN步数优势大。
- 节点数多:成百上千个节点时,Ring延迟不可接受。
- 跨数据中心:高延迟WAN网络中,减少通信步数极为关键。
- 层次化拓扑:自然匹配fat-tree网络架构。
Tree的缺点:
- 带宽利用率低:叶子节点带宽充分利用,但根节点附近成为瓶颈(需要处理所有子树的数据),总有效带宽约为Ring的 1/log(N)。
- 大消息时根节点带宽饱和,性能远不如Ring。
NCCL的策略:
- 消息 < 256KB:使用Double Binary Tree(两棵树交错,提高带宽利用率)。
- 消息 > 256KB:使用Ring。
- 实际阈值会根据拓扑和NIC数量动态调整。
Q: 手撕:反转矩阵?
(编程题)
Q: 手撕:合并两个有序链表?
(编程题)