基础知识
整理各种面试中常见的八股问题,涵盖 Python 基础、PyTorch 基础、算法与数据结构等内容
Python
基本特性
-
讲一下 Python 的深拷贝和浅拷贝
-
浅拷贝只复制顶层对象,嵌套的可变对象仍然引用原数据,修改会互相影响
-
深拷贝会递归复制所有层级,创建完全独立的新对象,修改互不影响
-
浅拷贝速度快、占用内存小;深拷贝速度慢、占用内存大
-
深拷贝示例
import copy b = copy.deepcopy(a) -
浅拷贝示例
# 浅拷贝三种写法 b = a.copy() b = list(a) # 或 dict(a) b = a[:] # 浅拷贝只复制顶层对象 import copy a = [1, 2, [3, 4]] b = copy.copy(a) # 浅拷贝 b.append(5) b[2].append(6) print(a) # [1, 2, [3, 4, 6]] print(b) # [1, 2, [3, 4, 6], 5]
-
-
讲一下 Python 的可变类型和不可变类型
-
不可变类型:对象创建后,值不能被修改;修改时会创建新对象,内存地址会变
-
可变类型:对象创建后,值可以直接修改;修改后内存地址不变
-
不可变类型包括:int、float、complex、bool、str、tuple、frozenset
-
可变类型包括:list、dict、set
-
不可变类型天然是线程安全的,不会被意外修改
a = 10 print(id(a)) a = 20 # 不是修改原值,而是创建新对象 print(id(a)) # 地址变了 -
可变类型示例
lst = [1,2] print(id(lst)) lst.append(3) # 直接修改原对象 print(id(lst)) # 地址不变 -
Python 中能作为字典 key、存入 set 的对象,必须是可哈希(hashable)的,而可哈希的前提通常是不可变类型
-
哈希表(字典、集合)底层通过哈希值(hash)快速定位元素位置,为了保证查找稳定、不冲突,要求 key 的哈希值不能变
-
-
讲一下 Python 的 *args 和 **kwargs
*args和**kwargs用于让函数接收任意数量的参数,让函数更灵活通用- 本质是解包语法:
*对应可迭代对象,**对应字典 *args用来接收任意多个位置参数(positional arguments),会把多余的位置参数打包成一个 tuple**kwargs用来接收任意多个关键字参数(keyword arguments),会把多余的关键字参数打包成一个 dict
-
讲一下 Python 的匿名函数和闭包
- 匿名函数
- 匿名函数就是没有名字的单行函数,用
lambda关键字定义,适合简单逻辑,不用写def - 它适合简单逻辑,常配合 map、filter、sorted 这些高阶函数使用,用完即销毁,不占用函数名
- 匿名函数就是没有名字的单行函数,用
- 闭包
- Python 中的函数可以作为参数、返回值,而函数作为返回值就是闭包的核心
- 闭包 = 内部函数 + 引用外部函数的变量 + 外部函数返回内部函数
- 让内部函数可以持续访问并保存外部函数的变量,即使外部函数已经执行完毕
- 三个形成条件
- 函数嵌套(外部函数里定义内部函数)
- 内部函数引用了外部函数的变量 / 参数
- 外部函数返回内部函数
- 外部函数执行完退出后,内部函数依然能访问自由变量
- Python 用 cell 对象(单元格) 来保存自由变量,不让它被回收
- 当内部函数引用外部变量时,Python 会把这个变量包装成一个 cell 对象
- 外部函数的栈帧销毁时,cell 对象不会被销毁
- 内部函数的
__closure__属性会持有这个 cell - 调用内部函数时,通过 cell 拿到原来的值
- 闭包的核心作用是可以保存外部函数的变量状态,延长变量生命周期,实现数据封装,也是 Python 装饰器的实现基础
- 匿名函数
-
讲一下 Python 的字典底层原理
- Python 3.7+ 字典有序,底层基于哈希表(散列表)实现
- 使用哈希表 + 紧凑索引数组,既保证 O (1) 访问,又节省内存
- 哈希冲突采用开放寻址法(线性探测)来解决
- 扩容:当负载因子达到 2/3 时自动扩容重排
- 3.6 后优化:先存索引再存数据,遍历顺序等于插入顺序,因此有序
-
讲一下 Python 中 is 和 == 的区别
==比较的是两个对象的值是否相等,调用__eq__方法is比较的是两个对象的内存地址是否相同(id 是否一致),判断是否是同一个对象- 小整数池、字符串驻留会让
is也返回 True,但这是缓存机制,不是普遍规则
-
讲一下 Python 的小整数池 & 字符串驻留
- 小整数池
- Python 启动时预先创建
[-5, 256]整数对象,全局复用 - 这些整数赋值时不会新建对象,而是直接引用池内对象
- 超过范围的整数不会缓存
- Python 启动时预先创建
- 字符串驻留(intern)
- 对符合标识符规则的字符串(字母、数字、下划线),Python 会缓存复用
- 相同字符串只存一份,节省内存、加快比较速度
- 长字符串、含特殊字符的字符串一般不驻留
- 小整数池
-
讲一下 Python 的偏函数 functools.partial
-
偏函数用于固定原函数的部分参数,生成一个新函数,简化调用
-
固定某些默认参数,不用每次重复传参
-
把多参函数转为少参函数,适配高阶函数(map、sorted 等)
from functools import partial def add(a, b): return a + b add2 = partial(add, b=2) add2(3) # 5
-
-
讲一下 Python 中闭包的 nonlocal
- 在闭包内部函数中,要修改外部函数的变量,必须声明
nonlocal;不声明则会被当成局部变量,报 UnboundLocalError nonlocal声明变量来自外层嵌套作用域,不是全局也不是局部global声明变量是全局的,而nonlocal声明变量是外层闭包的
- 在闭包内部函数中,要修改外部函数的变量,必须声明
装饰器
-
装饰器是 Python 中一种在不修改原函数代码、不改变调用方式的前提下,为函数动态添加额外功能的语法,本质上是基于闭包实现的高阶函数
-
装饰器就是一个函数,它接收被装饰函数,在内部包装一层逻辑,返回包装后的新函数
def decorator(func): def wrapper(*args,**kwargs): # 执行前增强 res = func(*args,**kwargs) # 执行后增强 return res return wrapper @decorator def f(): pass -
保留原函数元信息:直接装饰会导致
__name__、__doc__变成 wrapper,需要使用@wraps(func)from functools import wraps def decorator(func): @wraps(func) def wrapper(*args,**kwargs): return func(*args,**kwargs) return wrapper -
带参数的装饰器:需要再套一层函数接收参数
def decorator(arg): def inner(func): @wraps(func) def wrapper(*args,**kwargs): return func(*args,**kwargs) return wrapper return inner -
多个装饰器叠加的执行顺序:靠近函数先装饰,执行时从外到内
def decorator1(func): def wrapper(): print("开始执行 decorator1") func() print("结束执行 decorator1") return wrapper def decorator2(func): def wrapper(): print("开始执行 decorator2") func() print("结束执行 decorator2") return wrapper def decorator3(func): def wrapper(): print("开始执行 decorator3") func() print("结束执行 decorator3") return wrapper @decorator1 @decorator2 @decorator3 def func(): print("----- 原函数执行 -----") # 调用 func() # 结果 # 开始执行 decorator1 # 开始执行 decorator2 # 开始执行 decorator3 # ----- 原函数执行 ----- # 结束执行 decorator3 # 结束执行 decorator2 # 结束执行 decorator1
迭代器和生成器
- 迭代器
- 迭代器是实现了迭代协议的对象,用来惰性遍历一个序列,不用一次性把所有数据加载到内存
- 一个对象要成为迭代器,必须实现两个方法:
__iter__():返回迭代器对象本身__next__():返回下一个元素,没有元素时抛出StopIteration
- 迭代器的好处是惰性计算,节省内存;同时只能向前遍历,不能回头
for循环本质就是不断调用next()
- 可迭代对象(Iterable)
- 可迭代对象实现了
__iter__()方法,能返回一个迭代器 - 但其本身不一定能
next()
- 可迭代对象实现了
- 生成器
- 生成器是简化版迭代器,用更优雅的语法实现迭代器协议,本质还是迭代器
- 两种写法:
- 生成器表达式:
(x for x in range(10)) - 带
yield的函数:可以暂停 / 恢复执行
- 生成器表达式:
- 讲一下 Python 的 yield
-
函数里出现
yield,这个函数就不再是普通函数,而是生成器函数 -
调用时不立即执行,只返回一个生成器对象
-
执行到
yield时暂停函数,并返回值,下次next()时从暂停处继续执行,直到再次遇到 yield 或结束def my_generator(n): for i in range(n): yield i # 暂停并返回 i
-
多线程、多进程和协程
-
核心结论
- 多线程:适合 IO 密集型(网络、文件、数据库),受 GIL 限制,计算密集型效率不高
- 多进程:适合 CPU 密集型,能真正并行,开销大、数据共享麻烦
- 协程:用户态轻量级线程,单线程内切换,极高并发,完全无 GIL 问题
-
多线程(Thread)
-
一个进程内部可以开启多个控制流,共享进程资源,切换由操作系统调度
-
不同线程之间共享内存,通信简单,切换开销小
-
受 GIL(全局解释器锁)限制,即在 CPython 中,同一时刻只有一个线程执行 Python 字节码,所以计算密集型任务无法多核并行,因此比较适合 IO 密集型(请求、爬虫、文件读写)
-
模块:
threading、concurrent.futures.ThreadPoolExecutorimport threading def cpu_task(): x = 0 for _ in range(10**7): x += 1 threads = [threading.Thread(target=cpu_task) for _ in range(4)] for t in threads: t.start() for t in threads: t.join()
-
-
多进程(Process)
-
操作系统独立分配资源,每个进程有独立的 Python 解释器和 GIL
-
不同进程之间不共享内存,数据隔离,更安全
-
可以进行多核并行,适合 CPU 密集型(计算、图像处理、模型推理)任务
-
缺点是创建开销大,进程间通信复杂(Queue、Pipe、Manager)
-
模块:
multiprocessing、concurrent.futures.ProcessPoolExecutorfrom multiprocessing import Process def cpu_task(): x = 0 for _ in range(10**7): x += 1 processes = [Process(target=cpu_task) for _ in range(4)] for p in processes: p.start() for p in processes: p.join()
-
-
协程(Coroutine)
-
用户态轻量级线程,在单线程内由程序自己控制切换,不经过操作系统
-
极轻量,切换开销极小,而且是在单线程内并发,无 GIL 问题
-
遇到 IO 任务自动挂起让出执行权,不阻塞主线程,高并发能力远超线程,适合超高并发 IO 密集型(大量网络请求、网关、代理)
-
await:用于挂起当前协程,等待另一个协程 / 异步操作完成,期间线程可以做别的事
-
模块:
asyncio、async/await关键字 -
事件循环(Event Loop):协程的调度中心,负责监听所有 IO 事件(网络、文件、定时器)
import asyncio import time # 定义一个协程函数 async def task(name, delay): print(f"[{time.time()}] 任务 {name} 开始,等待 {delay}s") # await 挂起当前协程,不阻塞线程 await asyncio.sleep(delay) print(f"[{time.time()}] 任务 {name} 完成") async def main(): # 创建多个协程任务 t1 = task("A", 2) t2 = task("B", 1) # 并发运行 await asyncio.gather(t1, t2) # 运行事件循环
-
内存管理和垃圾回收
- 内存分配机制
- Python 底层通过内存池(PyMem_Malloc / PyObject_Malloc) 管理小对象,减少频繁向 OS 申请 / 释放内存,提升效率
- 小整数、小字符串会被 intern 机制缓存复用,避免重复创建
- 列表、字典等容器会预分配一部分空间,减少扩容开销
- 引用计数(Reference Counting)
- 每个对象内部维护一个
ob_refcnt引用计数值:- 对象被创建、被赋值、被当作参数传递 → 计数 +1
- 对象被删除
del、离开作用域、引用重新赋值 → 计数 -1 - 当引用计数 == 0,对象立即被销毁,内存释放
- 无法解决循环引用,计数增减有微小开销
- 每个对象内部维护一个
- 垃圾回收机制
- 为了解决循环引用,Python 引入了分代垃圾回收,三大策略
- 引用计数(基础)
- 标记 - 清除(Mark-Sweep)
- 分代回收(Generational Collection)
- 标记 - 清除算法:解决循环引用
- 扫描所有容器对象(list、dict、set、class…)
- 标记:从根对象(全局变量、栈引用等)出发,遍历所有可达对象并标记
- 清除:未被标记的对象判定为垃圾,直接回收
- 缺点是每次扫描全堆对象,效率较低,运行时会产生 STW(Stop-The-World),短暂阻塞程序
- Python 把对象分为 3 代:
- 0 代:新创建对象
- 1 代:0 代回收后存活的
- 2 代:1 代回收后存活的
- 存活越久的对象,越不可能是垃圾
- 为了解决循环引用,Python 引入了分代垃圾回收,三大策略
- 弱引用 weakref
- 弱引用(weakref)是一种不增加对象引用计数的引用方式,不会影响垃圾回收,专门用来解决循环引用、避免内存泄漏
- 强引用:赋值就会让引用计数 +1,计数不为 0 对象就不会被回收
- 弱引用:只 “指向” 对象,不改变引用计数,GC 可以正常回收对象
上下文管理器
-
上下文管理器是用来自动管理资源的机制,配合 with 语句使用,保证代码无论正常结束还是异常抛出,都能自动释放资源,避免资源泄漏
-
一个类只要实现了上下文管理协议,就是上下文管理器:
__enter__(self)with语句执行时进入时调用- 返回值会赋值给
as后面的变量
__exit__(self, exc_type, exc_val, exc_tb)- 代码块执行完毕退出时调用
- 无论是否发生异常都会执行
- 三个参数是异常信息,无异常则都为
None
-
代码示例
class MyContext: def __enter__(self): print("进入上下文,分配资源") return self def __exit__(self, exc_type, exc_val, exc_tb): print("退出上下文,释放资源") # 这里可以处理异常 # return True 表示异常已被处理,不会向外抛 with MyContext() as f: print("执行代码块") -
更简单的写法:
contextlib.contextmanageryield之前 =__enter__yield之后 =__exit__
from contextlib import contextmanager @contextmanager def my_context(): print("进入上下文") yield # 暂停,交出执行权 print("退出上下文") with my_context(): print("运行中")
单例模式、工厂模式、观察者模式
-
单例模式
-
单例模式是一种创建型设计模式,保证一个类全局有且仅有一个实例,并提供一个全局访问点,多次实例化都返回同一个对象
-
用于避免重复创建对象造成资源浪费,保证数据一致性(如数据库连接、日志对象)
-
用
__new__实现class Singleton: _instance = None def __new__(cls, *args,**kwargs): if not cls._instance: cls._instance = super().__new__(cls) return cls._instance a = Singleton() b = Singleton() print(a is b) # True -
用装饰器实现
def singleton(cls): instances = {} def wrapper(*args,**kwargs): if cls not in instances: instances[cls] = cls(*args,**kwargs) return instances[cls] return wrapper @singleton class A: pass
-
-
工厂模式
-
工厂模式是创建型设计模式,把对象的创建与使用分离,由工厂统一负责实例化对象,使用者无需关心具体类和创建细节
-
简单工厂
class Product1: pass class Product2: pass class SimpleFactory: @staticmethod def create(product_type): if product_type == 1: return Product1() elif product_type == 2: return Product2() obj = SimpleFactory.create(1)
-
-
观察者模式(发布 - 订阅)
-
一个对象(主题)状态变化时,自动通知所有依赖它的观察者
class Subject: def __init__(self): self._observers = [] def attach(self, observer): self._observers.append(observer) def detach(self, observer): self._observers.remove(observer) def notify(self, msg): for obs in self._observers: obs.update(msg) class Observer: def update(self, msg): print("收到消息:", msg) # 使用 sub = Subject() obs1 = Observer() obs2 = Observer() sub.attach(obs1) sub.attach(obs2) sub.notify("状态已更新") # 自动通知所有观察者
-
PyTorch
基本特性
- 讲一下 view、reshape 之间的区别?contiguous 的作用?
- view:必须要求张量是连续 (contiguous) 的,非连续张量会直接报错;仅做视图操作,不开辟新内存,和原张量共享数据
- reshape:不要求连续,张量连续时等价于 view(共享内存);张量不连续时,会自动拷贝数据生成连续张量,开辟新内存
- contiguous:让非连续张量变成内存连续存储的张量,开辟新内存拷贝数据;只有连续张量才能使用 view、permute 后部分操作等,解决 view 报错的核心方法
- 讲一下 permute 和 transpose 的区别?
- transpose:只能交换两个维度,单次操作仅调换 2 个轴的顺序
- permute:可以一次性交换任意多个维度,支持重排所有维度
- 二者都不会改变数据存储,仅改变张量视图,且都会让张量失去连续性
- 什么是计算图?PyTorch 中反向传播的流程?detach 的作用?
- 计算图是 PyTorch 自动微分的核心数据结构,以张量为节点、以运算为边,记录张量从输入到输出的所有计算链路,用于自动推导梯度
- 反向传播流程
- 前向传播:计算输出,自动构建计算图
- 调用
loss.backward():从损失节点开始,链式法则反向计算所有叶子张量的梯度,并累加到grad属性 - 优化器更新参数:利用计算好的梯度更新模型参数
- detach 将张量从计算图中剥离,切断梯度回传链路,分离后的张量无梯度、不参与反向传播,常用于冻结参数、提取中间结果
- 什么是叶子张量 / 非叶子张量?
- 叶子张量是用户手动创建、未经过任何运算的张量(如模型参数
nn.Parameter、手动初始化的requires_grad=True张量),是梯度计算的终点,反向传播后会保留梯度 - 非叶子张量是由叶子张量经过运算得到的中间张量,反向传播后梯度默认被释放,不保留(可通过
retain_grad()保留)
- 叶子张量是用户手动创建、未经过任何运算的张量(如模型参数
- 如何禁止反向传播的梯度计算?
- torch.no_grad ():上下文管理器,最常用,包裹代码块内所有张量都不计算梯度,节省内存 / 加速
- requires_grad=False:创建张量时直接设置,永久关闭该张量梯度
- detach ():将张量从计算图分离,切断梯度传播
- optimizer 为什么要进行 zero_grad?
- PyTorch 梯度会默认累加(不自动清零),如果不调用
optimizer.zero_grad(),上一轮迭代的梯度会累加到当前轮,导致梯度错误、模型无法收敛
- PyTorch 梯度会默认累加(不自动清零),如果不调用
- 为什么要进行梯度裁剪?
- 解决深度网络训练中的梯度爆炸问题(梯度过大导致参数更新失控、loss 震荡 / 发散)
- 强制将梯度限制在设定阈值内,稳定训练过程,保证模型收敛
- in-place 操作是什么?
- 不创建新张量,直接在原内存地址上修改张量值的操作(变量名不变,值被覆盖)
x += 1、x.add_(1)(带_的都是 in-place)- 极易破坏计算图,导致反向传播报错,自动微分场景禁止使用
- BF16 和 FP16 的区别?有什么风险?FP16 为什么更需要通过 scale 避免梯度下溢?
-
对比
特性 FP16 BF16 位宽 16 位 16 位 精度 (尾数) 10 位(精度更高) 7 位(精度更低) 动态范围 小 大(接近 FP32) 硬件支持 NVIDIA GPU 专用 新一代 CPU/GPU 通用 -
主要风险是精度溢出 ——FP16 易梯度下溢,BF16 易精度损失,二者都数值不稳定,可能导致训练不收敛、精度下降
-
FP16 表示范围小,反向传播时梯度值很小,会直接变成 0(梯度下溢),因此需要乘以一个 scale 系数放大梯度,避免下溢,更新参数前再还原,保证梯度有效
-
推理
-
PyTorch 中的动态图和静态图的区别?动态图每次 forward 都重建,不会很慢吗?
- 动态图(Eager Execution)
- 边执行、边建图,前向时实时构建计算图,每次 forward 图结构可随输入 / 控制流动态变化
- 灵活、易调试,支持原生 Python if/for/while;无预编译,依赖 Python 解释器
- 静态图(TorchScript/ONNX)
- 先编译建图、后执行,图结构固定,可脱离 Python 运行
- 性能高、可部署,支持全图优化(算子融合、内存复用);灵活性差
- 动态图每次重建会不会很慢?
- PyTorch 动态图只记录计算依赖,构图开销极小(微秒级)
- 真正耗时是 GPU 算子执行,构图开销可忽略,训练 / 推理瓶颈在计算,不在构图
- 可以通过 torch.compile 等转为静态图
- 动态图(Eager Execution)
-
PyTorch 如何导出静态图?都是什么格式的?
- TorchScript(torch.jit)可以通过两种方式导出静态图
- trace:给样例输入,跑一次记录执行流;不记录控制流(只保留执行时的分支)
- script:解析源码,支持 if/for;要求类型 / 形状可推导
- 格式:.pt/.pth(序列化 ScriptModule)
- torch.export(PyTorch 2.x 新)
- 更严格的静态单图,支持动态形状符号化、无 graph break
- 输出 ExportedProgram,可序列化 / 跨框架
- ONNX
- 用
torch.onnx.export / dynamo_export导出为 ONNX 格式(.onnx) - 开放标准,适配 ONNX Runtime、TensorRT、OpenVINO 等
- 用
- TensorRT
- 接收模型(ONNX / TorchScript),解析、固化成静态计算图,并进行进一步的图优化(算子融合、消除冗余节点、显存复用、多分枝优化、选择最优 kernel),最后生成一个不可变、高度优化的静态引擎(.engine/.plan)
- TorchScript(torch.jit)可以通过两种方式导出静态图
-
PyTorch 如何部署推理模型的?
-
导出 TorchScript
traced_model = torch.jit.trace(model, example_input) torch.jit.save(traced_model, "model.pt") -
下载 libtorch(C++ 推理库)
-
C++ 加载模型
torch::jit::Module module = torch::jit::load("model.pt"); module.to(torch::kCUDA); -
构造输入 → 前向推理
at::Tensor input = torch::randn({1, 3, 224, 224}).cuda(); std::vector<torch::jit::IValue> inputs; inputs.push_back(input); at::Tensor output = module.forward(inputs).toTensor(); -
也可以直接在 python 中加载部署
model = torch.jit.load("model.pt") model.cuda() model.eval() with torch.no_grad(): out = model(x) -
也可以转为 TensroRT engine 部署
import torch_tensorrt trt_model = torch_tensorrt.compile( traced_model, inputs=[x], enabled_precisions={torch.half} ) trt_model.save("model.trt") -
也可以使用 Triton/TorchServe 服务化部署
-
-
PyTorch 导出模型有哪几种方式?
- 权重保存(仅参数,非图):
torch.save(model.state_dict(), "xxx.pth"),只保存参数,不存储计算图,加载时需要重新定义模型代码 - torch.compile(内存编译,非导出):不存文件,运行时加速;生成优化后 kernel,适合训练 / 推理加速
- 权重保存(仅参数,非图):
-
什么是 CUDA Graph?与前面的静态图有什么区别?
-
CUDA Graph 是 NVIDIA GPU 层面的技术,把一整步(forward/backward/optim)的所有 CUDA kernel、memcpy 预先捕获(Capture)成一个静态有向无环图(DAG),在执行时进行 Instantiate(实例化)编译成可执行图
-
一次捕获、多次重放,重放时 CPU 只发 1 次指令,GPU 自己按图跑完所有算子,能够大幅降低 kernel launch 开销(小模型 / 小 batch 明显)
-
基本流程
- Capture 定义图:得到
cudaGraph_t(图定义,类似蓝图) - Instantiate 实例化编译:把图验证、优化、转成可执行格式,得到
cudaGraphExec_t(可执行图) - 重放 Replay(执行):用 cudaGraphLaunch 一次启动整张图,开销极低,接近 launch 单个 Kernel 的成本
- Capture 定义图:得到
-
CUDA Graph 的节点类型
- Kernel 节点:GPU 算子(核心)
- Memcpy / Memset:设备内 / 主机 - 设备拷贝
- Host 回调:CPU 函数(极少用)
- 子图:嵌套图
- 事件 / 信号量:同步节点
-
代码示例
import torch model = model.cuda() x = torch.randn(1, 3, 224, 224, device='cuda') # 1. 预热(必须,稳定上下文) for _ in range(3): _ = model(x) # 2. 录制 CUDA Graph g = torch.cuda.CUDAGraph() with torch.cuda.graph(g): y_static = model(x) # 只记录GPU操作 # 3. 推理/训练循环:重放! for real_data in dataloader: x.copy_(real_data) # 原地覆盖输入数据 g.replay() # 极快! output = y_static # 结果在y_static -
PyTorch 2.x 可以自动集成 CUDA Graph
- torch.compile (mode="max-autotune") 会自动用 CUDA Graph
- torch.cuda.make_graphed_callables ():包装函数自动图化
-
缺点
- 类似 torch.jit.trace,完全静态,张量的形状、尺寸、内存地址必须固定,控制流也是固定的,重放时无法改变
- 只能包含 GPU 操作,不能包含 CPU 同步(
.item(),print(x),len(x)),不能动态分配显存(cudaMalloc) - 输入必须原地更新:不能新建张量,必须
x.copy_(data)覆盖原有地址
-
区别
维度 CUDA Graph 框架静态图 层级 GPU 运行时(CUDA 核函数 / 内存) 深度学习框架(张量 / 算子 / 控制流)NVIDIA 范围 单 step 内所有 GPU 操作 模型前向计算逻辑(含网络结构)NVIDIA 控制流 完全不支持(结构必须固定) TorchScript script/ONNX 支持 if/loop 优化 减少 CPU 启动开销 算子融合、内存复用、常量折叠 NVIDIA 部署 同 GPU、同内存布局 跨平台、跨设备、跨框架 NVIDIA 关系 可叠加:静态图 + CUDA Graph 双重加速
-
-
PyTorch 静态图对控制流的支持如何?如何适配控制流?
- torch.jit.trace:完全不支持,只记录样例执行分支,if/for 被固定,其他分支丢失
- torch.jit.script:支持
if/for/while,但条件必须是编译期可推断(Python 标量 / 模型属性);禁止张量值条件(如if x.sum() > 0)、动态形状依赖 - ONNX(传统):同 trace,不支持数据依赖控制流
- 如何适配控制流
- 用 scripting 代替 trace(适合简单 if/for)
- 条件改为编译期常量(self.flag/torch.jit.is_scripting ())
- ONNX + 控制流算子:导出带 If/Loop 的 ONNX 图
-
生产部署带控制流的模型,使用哪种方式?
- 首选 TorchScript scripting + C++/LibTorch 部署,适合规则明确、分支少的模型
- torch.export + 推理后端:严格静态、无 graph break、动态形状友好
-
什么是 Triton?
- Triton(Triton Inference Server) 是 NVIDIA 开源的高性能、多框架、跨平台的 AI 模型推理服务引擎,专门解决深度学习模型在生产环境中的部署、调度、性能与运维问题
- 支持
- 框架:PyTorch (TorchScript)、TensorRT、ONNX、TensorFlow、OpenVINO、Python 后端 NVIDIA
- 硬件:NVIDIA GPU、x86/ARM CPU、AWS InferentiaNVIDIA
- 接口:HTTP/REST、gRPC、C API
机器学习基础
激活函数
- ReLU、GeLU、SwiGLU 历史,ReLU 的缺点
-
sigmoid
- 即
- 输出 0~1,会梯度消失,几乎不再单独使用
- 只用来做门控,不做特征激活
-
ReLU 最古老(2011)
- 即
- 死亡神经元:一旦输入负数,输出 = 0,梯度 = 0,神经元永远不更新,直接 “死了”
- 输出不是零中心化:导致梯度下降震荡
- 负数信息完全丢失:信息损失大
-
SiLU(Swish)
- 即
- 正数:接近 x(线性)
- 负数:平滑下落到 0(不是直接截断)
- 处处可导,没有死神经元
-
GELU (2016 年,GPT、BERT 用)
- 即 , 是标准正态分布的 CDF
- 高斯误差线性单元,平滑、柔和、可导
- 不会死亡神经元,训练更稳定
-
门控线性单元的结构
x ──→ linear1 → activation → gate x ──→ linear2 → value gate * value → output -
SwiGLU(2020 年之后,LLaMA、所有大模型都用,门控线性单元)
- 不是普通激活函数,是门控线性单元
- 即
- 门控机制,信息流动更精细,训练更稳、收敛更快
-
GeGLU(门控线性单元)
- 即
-
优化器
-
梯度下降优化器(SGD/Momentum/Adam/AdamW/Lion)的原理、区别?
- 梯度下降优化器就是用来最小化损失函数、迭代更新模型参数的算法,核心思想是沿着损失函数梯度的反方向更新参数,一步步走到局部 / 全局最优
- 本质:控制参数更新的步长与方向,让模型收敛更快、更稳、泛化更好
- 梯度下降优化器本质都是沿着负梯度方向更新参数,区别在于是否用动量、是否做自适应学习率、是否对权重衰减正确处理、以及更新方式(一阶矩 / 二阶矩 / 符号更新)
- 符号定义
- :当前这一步的梯度(方向 + 大小)
- :学习率
-
SGD
- 每次用单个样本 / 小批量计算梯度,实现简单
- 梯度噪声大,收敛慢,容易在鞍点 / 局部最优徘徊
- 而后续改进也主要是解决两个问题:
- 让方向更稳,不乱抖 → 一阶动量做的
- 让每个参数有自己合适的步长 → 二阶矩做的
-
SGD + Momentum
- 梯度不是 “只看当前一步”,而是把最近几步的梯度加起来平均一下
- 引入动量,累积历史梯度方向,能够让震荡方向被平滑掉,前进方向被加强
- 减小梯度震荡,加速收敛,能冲出局部最优和平坦区域
-
RMSProp
- 对每个参数,记录它历史梯度的平方和,代表这个参数梯度 “大不大”
- 梯度一直很大的参数 → 分母变大 → 步长缩小
- 梯度一直很小的参数 → 分母变小 → 步长放大
- 每个参数的学习率都不一样:
- 稀疏特征、小梯度 → 学习率自动变大
- 密集特征、大梯度 → 学习率自动变小
- 通过加入二阶估计,自适应学习率,解决不同参数梯度尺度差异大的问题
- 对每个参数自适应缩放梯度,适合稀疏梯度、非凸优化
- 对每个参数,记录它历史梯度的平方和,代表这个参数梯度 “大不大”
-
Adam(自适应矩估计)
-
结合 Momentum + RMSProp,维护一阶矩(梯度均值,动量)、二阶矩(梯度平方均值,自适应学习率)
-
对一阶、二阶矩做偏差修正,解决初期偏置问题,每个参数自适应调整学习率
-
收敛极快,对超参不敏感,可能泛化不如 SGD,容易过拟合
-
梯度带 L2
-
一阶矩估计(动量)
-
二阶矩估计(RMSprop)
-
偏差校正
-
参数更新
-
L2 项 被包含在梯度里,一起被 Adam 缩放,正则强度被扭曲
-
-
Adam 为什么要做偏差矫正?
-
常用 ,,而因为 、 初始化为 0,在训练刚开始几步时,一阶、二阶矩估计会系统性偏小、偏向 0,导致更新幅度被低估
-
偏差矫正就是把这个低估的部分 “补回来”,让前期训练更稳定
-
以一阶矩为例,期望展开:
-
这是一个指数移动平均,真实梯度被系数 压制,当 t 很小时:
-
二阶矩同理:
-
-
直接用原始值更新,会导致前期步长过小,训练启动很慢,容易陷入局部最优
-
而偏差矫正就是除以 “累积权重和”,把被压低的期望拉回真实值,而由于 和 都小于 1,因此随着 越大,其 次方越趋近于 0,分母趋近于 1,校正放大效果几乎消失
-
-
AdamW
-
原始 Adam 把权重衰减(Weight Decay)和学习率混在一起,AdamW 把两者解耦,权重衰减直接作用于参数,不通过梯度计算
-
缓解 Adam 过拟合问题,泛化能力显著提升,现在训练 Transformer、大模型首选 AdamW
-
只计算原始梯度
-
动量 & 二阶矩(同 Adam)
-
偏差校正(同 Adam)
-
关键:独立权重衰减 + 更新
-
最后一项 就是独立权重衰减,不参与动量、二阶矩计算
-
-
Lion(EvoLved Sign Momentum)
-
基于符号动量,不使用梯度本身,只用梯度的符号()更新;内存更小,只维护一阶动量,无二阶矩
- :只存一个动量
- :符号函数 → 输出 +1 / 0 /-1
- :解耦权重衰减(同 AdamW)
-
用 sign () 代替自适应缩放
- Adam:、 → 每个参数步长不一样
- Lion: → 所有参数步长大小相同,只有方向不同
-
为什么 sign () 反而更好?
- 抗噪声:梯度噪声大时, 只看方向、不看大小 → 更稳
- 步长均匀:不会出现某些参数步长爆炸、某些几乎不动
- 计算更快:少了除法、开方、平方 → 速度快 10%~15%
-
训练大模型、视觉模型效果 often 优于 AdamW,对学习率更敏感,需要精细调参
- 必须调小学习率( 而非 )
- 小模型 / 小 batch 有时不如 Adam 稳
- 稀疏特征场景略弱(因为没有自适应缩放)
-
-
Muon(Momentum Orthogonal Newton)
-
核心思想
- Adam:把权重展平成向量,按单个参数逐元素更新,易出现方向冲突、冗余计算
- Muon:把权重当成矩阵(如 Transformer 的 dmodel×dff),对动量做正交化,让更新方向 “彼此垂直、幅度一致”,消除冗余探索
- 核心:让更新矩阵尽可能正交 → 防止各方向互相干扰、加速收敛
-
对权重矩阵 ,动量矩阵 也是 ,每行代表一个参数向量的动量
- :动量矩阵
- Newton-Schulz:快速迭代求 正交近似(),将动量矩阵转为最近正交矩阵(等价于 SVD 后将奇异值矩阵置为单位阵,仅保留方向)
- 结果:更新方向互相垂直、不互相抵消,各方向均匀收敛、不易震荡、更快到全局最优
-
-
大规模 LLM 如何用 Muon?
- 混合优化:输入 / 输出层 / 偏置 / LayerNorm 用 AdamW,隐藏层用 Muon(加权重衰减 + 逐参数 LR 校准);配合 ZeRO-1 分布式,显存减半、通信可控
- 稳定技巧:大模型加 MuonClip(QK-Clip),限制注意力头逻辑值,避免训练 “飙车失控”
-
AdamW 相比 Adam 的改进?
- AdamW = 修正了 Adam 中 “权重衰减计算错误” 的版本
- Adam 里的权重衰减(L2 正则)是在梯度更新之后加的,导致正则效果被学习率影响,变得不稳定
- AdamW 改进
-
把权重衰减独立出来,不跟梯度、动量混合
-
直接对参数做衰减
-
正则效果更稳定、泛化更好,不会因为学习率变化而失效
-
-
正则化(L1/L2/Dropout)的作用?大模型中的正则化技巧?
- 过拟合的本质之一就是模型用了太多无关特征、噪声特征去硬记训练集,而正则化就是给模型加约束,防止它过度拟合训练数据,提升泛化能力
- L1 正则化在目标函数中加入权重的绝对值和
- 梯度固定为 ,不管 w 多小,都持续往 0 拉,从而把大量不重要特征的权重直接压到 0,相当于自动扔掉没用 / 噪声特征,而只保留真正有用的特征,进而达到稀疏、特征选择的目的
- L1 正则通过把噪声 / 无关特征的权重变成 0,模型只依赖少数强特征,模型结构变简单,自然就不容易过拟合,其梯度为 sign 函数
- 适合要稀疏、要特征选择的场景
- L2 正则化在损失函数中加入所有权重的平方和,其梯度即为参数本身
- 在梯度下降时,相当于每次更新都先把权重朝 0 方向缩小一点,让模型权重不会过大、函数更平滑,从而抑制过拟合,提升泛化能力
- 适合要稳定、平滑、防过拟合、好训练的场景
- Dropout 从参数正则化转向到结构正则化,Dropout 并不是在目标函数中显式加项,而是在训练阶段随机破坏网络结构
- 过拟合的一个重要原因是神经元之间互相依赖、互相配合去记忆噪声,而通过 Dropout,每次训练,网络结构都不一样,从而防止共适应
- 其他正则化手段
- 数据层面:增加训练数据的多样性、数据清洗 / 去噪
- 模型 / 训练策略:早停(Early Stopping)、权重共享(Weight Sharing)、标签平滑(Label Smoothing)
-
什么是标签平滑?
- 标签平滑是一种正则化手段,将原本 one-hot 硬标签 [1,0,0...] 替换为软标签,把一小部分概率 均匀分给其他类别
- 目的是防止模型对训练样本过度自信、过拟合,让模型学习更泛化的决策边界,同时让输出分布更平滑,提升鲁棒性和泛化能力,常用于分类、检测等任务
- 传统 CV/NLP 分类任务中,固定类别数 C(1000、10000 类),一个样本只对应一个硬标签,模型容易对标签过度自信、过拟合,所以用标签平滑做正则,让分布软一点
- 而大模型的训练其任务不是分类任务而是序列生成 / 语言建模,自回归预测下一个 token,每个位置都是一个多分类任务,在这种极端分类的情况下,正确 token 概率本来就很小,模型天然很难 "极端自信",标签平滑的正则效果微乎其微
- 相反地,标签平滑会直接损害困惑度(PPL)与建模能力,因为 LLM 的核心目标是精准你和真实文本的分布,标签平滑反而把正确 token 的概率挖掉一块强制分给所有其他无关 token,使得模型不敢给正确词高分,从而使得困惑度变差
-
LR
- 为什么必须 warmup?
- 前期模型参数随机、不稳定,直接用大 LR 会震荡、不收敛甚至 NaN,而 Warmup 让学习率从极小慢慢升到最大值,保证训练稳定启动
- 为什么必须 decay?
- 后期模型接近收敛,需要减小步长,精细微调权重,防止在最优解附近震荡,提高最终精度
- 同时也是一种隐式正则,缓解过拟合 ——LR 越来越小,权重更新幅度变小
- LR 的 decay 策略有哪些?
-
阶梯衰减
-
cosine decay
-
多项式衰减
-
指数衰减
-
inverse sqrt /rsqrt decay(倒数平方根)
-
- 为什么必须 warmup?
-
Batch 越大,为什么梯度越平滑?
- 单个样本的梯度是有噪声的,每个样本的梯度方向不一定指向全局最优,会乱跳
- 而大的 batch,则是对更多样本做无偏平均,噪声本身是零均值的,样本越多,噪声互相抵消越多
- 同时,统计上,方差与 batch 大小成反比,batch 越大,方差越小,梯度越平滑、抖动越小
-
什么是梯度累积?
- 梯度累积就是扩大有效的 batch size,不增加显存占用,但是权重更新次数变少,相同步数下,迭代次数减少,收敛变慢
- 有效 LR ≈ 原 LR × 累积步数,大 batch 训练通常要线性放大 LR(linear scaling rule)
- 大 batch 进行一次前向,统计更同步,批次归一化 BN 更准确
- 而梯度累积分多次小 batch 算 BN,均值方差是分段统计的,统计有偏差,训练稳定性和最终效果略差于真实大 batch
- SGD 是在线更新 —— 算一个 batch 就更一步,及时纠正方向;而梯度累积是攒够梯度再更新,中间模型参数一直不动,梯度信息偏旧,优化路径不如实时更新干净
- 真大 batch 往往泛化更好、更稳定,梯度累积只是模拟大 batch 的梯度量级,没有完全等价的训练效果
-
batch size 变大学习率怎么调整?
- 线性缩放规则:Batch Size 扩大 k 倍 → 学习率也扩大 k 倍
- Batch 变大 → 梯度估计更稳定、噪声更小,所以可以更大步更新,更快收敛
- 不调 lr → 训练变慢、容易陷入局部最优
- 调太大 → 震荡、不收敛
其他
-
交叉熵损失的计算细节?
-
公式
-
第一步,先对 logits 做 softmax,把输出改为概率分布
-
第二步,取真实标签对应的概率
-
第三步,取负对数
-
PyTorch 的 cross_entropy 自带 softmax
-
-
什么是 Softmax Margin Loss
- 在标准 Softmax 交叉熵基础上,人为加入 “间隔(Margin)” 的一类改进损失,核心目的是让类内更紧凑、类间更分离,学习判别性更强的特征(尤其用于人脸识别、细粒度分类、度量学习)
-
什么是 pass@k /cons@k?
- pass@k:对单个问题,模型生成 k 个候选答案,只要 至少 1 个正确,该问题记为 “通过”
- cons@k:对单个问题,模型生成 k 个候选答案,要求:
- 多数一致:k 个结果中 大多数相同(多数投票)
- 结果正确:这个 “多数结果” 必须是正确答案
-
什么是精确率 (Precision) 和召回率 (Recall)?什么是 F1?
- TP(真阳性):预测有,且确实对
- FP(假阳性):预测有,但实际错 / 多余
- FN(假阴性):实际有,但没预测到
- Precision(精确率):预测出来的东西里,多少是对的
- Recall(召回率):标准答案里的东西,找回了多少
- F1 同时考虑精确率和召回率,避免只准不全或只全不准
-
三种浮点格式:符号位 + 指数位 + 尾数位
类型 符号 指数 尾数 总 bit 特点 FP32 1 8 23 32 精度最高、范围大 FP16 1 5 10 16 精度还行,但范围小 BF16 1 8 7 16 范围和 FP32 一样,精度低
分布式基础
基础知识
- 什么是异构计算?
- 同一系统中使用不同架构的处理器协同完成计算:CPU(通用控制) + GPU(高并行计算) + NPU/TPU(专用加速)
- 优势:用专用芯片做擅长的事,实现性能、功耗、吞吐量的最优平衡
- 大模型场景:CPU 负责调度、IO、数据加载;GPU 负责矩阵计算与通信
- 异构系统的核心层次(自上而下)
- 应用层:Transformer 训练 / 推理、数据 pipeline、通信调度
- 框架层:PyTorch、TensorFlow、DeepSpeed、Colossal-AI
- 算子加速层:CUDA Kernel、FlashAttention、cuBLAS、cuDNN、Triton
- 运行时层:CUDA Runtime、NCCL、MPI、UCC
- 硬件层:GPU、HBM、显存带宽、NVLink、PCIe、Infiniband
- 整个系统的性能瓶颈往往不在算力,而在访存、通信、调度
- 高性能计算的三大核心指标
- 计算吞吐量:FLOPs/s、TFLOPS
- 访存带宽:GB/s(HBM2e/HBM3 是核心瓶颈)
- 通信带宽:GB/s(NVLink、IB、RoCE)
- 大模型训练是典型的带宽受限任务,而非算力受限
- GPU 为什么适合深度学习?
- 单指令多线程 SIMT/SIMD:极高并行度
- HBM 高带宽显存:远高于 CPU 内存
- Tensor Core 专门加速矩阵乘(GEMM)
- 硬件支持 AllReduce、集体通信
- GPU 擅长高密度、高并行、高访存的重复计算
- 性能瓶颈与优化方向
- 计算瓶颈:低算力利用率,用混合精度、Tensor Core
- 访存瓶颈:HBM 带宽不够,用 FlashAttention、算子融合
- 通信瓶颈:跨卡跨机慢,用 NVLink、IB、压缩通信、拓扑感知调度
- IO 瓶颈:数据加载慢,用 prefetch、多进程、内存映射
显存 / 内存拷贝模型
- 两个域
- Host:CPU 内存(DRAM,主存)
- Device:GPU 显存(VRAM)
- H2D = Host to Device:数据加载、输入张量拷贝、权重从 CPU 载入 GPU、optimizer 状态初始化
- D2H = Device to Host:loss 打印、metric 同步、checkpoint 保存、推理结果回传、debug 打印张量
- D2D = Device to Device
- 同卡 D2D:显存内部搬运(极快,无外部带宽消耗)
- 跨卡 D2D:NVLink/RDMA 直传(DDP、AllReduce 核心流量)
- H2H = Host to Host:普通内存拷贝,和 GPU 无关,一般不关心
- 带宽举例
- D2D(NVLink):~900 GB/s 极速
- D2D(PCIe 5.0):~64 GB/s 瓶颈
- H2D / D2H(PCIe):~64 GB/s 瓶颈
- 跨机 RDMA (100G IB):~12.5 GB/s 最慢长途
- H2D 为什么永远是瓶颈?为什么大模型禁止频繁 D2H 打印 tensor?
- PCIe 带宽远低于 VRAM/NVLink
- 频繁 D2H 会触发同步等待、清空 cache、阻断异步流,吞吐暴跌
通信硬件
- PCIe(Peripheral Component Interconnect Express):通用服务器内部总线,CPU ↔ GPU/NIC/ 存储的基础通道,单机内基础互联
- 参数
- PCIe 4.0 x16:单向~32 GB/s,双向~64 GB/s
- PCIe 5.0 x16:单向~64 GB/s,双向~128 GB/s
- 特性
- 单机内:CPU ↔ GPU 数据加载、控制指令、非密集型通信
- 多机:通过 PCIe 连接 IB 网卡(HCA),作为 RDMA 通信的物理出口
- 局限:带宽 / 延迟远低于 NVLink;不支持 GPU 间 P2P 直连(需 CPU 中转);不原生支持 RDMA
- 参数
- NVLink:NVIDIA 专有的 GPU 间高速直连,单机内 GPU 高速互联,替代 / 补充 PCIe
- GPU ↔ GPU、GPU ↔ CPU 的专用高速直连,单机内 GPU 通信的 “高速公路”
- 参数
- NVLink 3.0(A100):单链路 50 GB/s 单向,每 GPU 12 链路 → 总 600 GB/s 单向
- NVLink 4.0(H100):单链路 50 GB/s 单向,每 GPU 18 链路 → 总 900 GB/s 单向
- NVLink 5.0(B200):单链路 100 GB/s 单向,总带宽达 1.8 TB/s 双向
- 特性
- GPU Direct P2P:GPU 间直接通信,绕过 CPU / 内存,延迟降至纳秒级
- 统一显存池:多 GPU 显存逻辑聚合(如 8×H100=640GB),支撑超大模型张量并行
- 网络内计算(In-Network Computing):NVSwitch 内直接做 AllReduce,大幅降低同步延迟
- 局限:NVIDIA 专有;仅用于单机 / 同 NVSwitch 域;无法跨节点扩展
- NVSwitch:NVLink 的交换芯片,实现单机内多 GPU 全互联(Full Mesh),构建单机 GPU 高速域
- 参数
- 单机 8/16 GPU 全互联,任意 GPU 对可同时通信,无阻塞
- 支持 NVLink 域扩展:多 NVSwitch 级联,构建更大单机 GPU 集群(如 DGX SuperPOD)
- 总带宽极高:NVL72 系统总带宽 130 TB/s,支撑 72 个 GPU 协同
- 单机内张量 / 流水线并行的通信基石,解决多 GPU 全互联带宽瓶颈,与 NVLink 一起构成单机 GPU 高速通信子系统
- 参数
- RDMA(Remote Direct Memory Access):远程直接内存访问技术,零拷贝、内核旁路、低延迟,是跨节点高速通信的核心能力
- 通信能力 / 协议,而非物理硬件;实现远程内存直接读写,零拷贝、内核旁路、低延迟
- 特性
- 数据直接在网卡(HCA)与 GPU 显存间传输,不经过 CPU / 内核 / 系统内存
- 配合 GPU Direct RDMA:GPU ↔ GPU 跨节点直连,延迟降至微秒级
- 跨节点 AllReduce、Broadcast、P2P 等集体通信的底层引擎,是 NCCL(NVIDIA 集体通信库)实现高效多机训练的核心依赖
- 主要跑在 InfiniBand 上,也可跑在 RoCE(RDMA over Converged Ethernet)上
- InfiniBand(IB):支持 RDMA 的专用高速网络,跨节点 / 集群级互联,大模型多机训练的主流网络
- 专为 HPC/AI 设计的专用高速网络,完整支持 RDMA,大模型多机训练的事实标准
- 参数
- HDR:400 Gb/s 每端口(双向~800 Gb/s)
- NDR:800 Gb/s 每端口
- XDR:1.6 Tb/s 每端口
- 每服务器配 4/8 张 IB 网卡(HCA),通过 IB 交换机构建无阻塞胖树(Fat-Tree)
- 配合 GPU Direct RDMA,实现 GPU 跨节点直连通信
- HPC(High Performance Computing)
- 极高算力、极高通信带宽 + 极低延迟、极强并行软件栈
- 以 CPU 为主,MPI 通信,以太网 / InfiniBand
- RoCE
通信软件
- MPI(Message Passing Interface)
- 通用并行计算通信协议,CPU/GPU 都能用,负责任务启动、进程管理、基础通信
- 对 GPU 通信没有专门优化
- 不感知 NVLink、NVSwitch、GPU P2P
- 直接用 MPI 做 GPU 集体通信,速度远不如 NCCL
- NCCL(NVIDIA Collective Communications Library)
- NVIDIA 官方、专为多 GPU 分布式训练优化的底层通信库
- PyTorch DDP、DeepSpeed、Megatron 背后真正的加速引擎
- 自动识别拓扑:单机内走 NVLink / NVSwitch,跨机走 InfiniBand/RoCE(RDMA)+ GPU Direct RDMA
- 优化的集体通信:AllReduce / AllGather / ReduceScatter / P2P
- 与 GPU 调度深度融合:支持 cuda stream,不阻塞计算,支持环算法、树形算法、NVSwitch 全互联拓扑
- 基本只针对 NVIDIA GPU,不负责进程启动、环境初始化,需要和 MPI 或其他启动器配合
- torchrun
- torchrun = PyTorch 官方自带的分布式任务启动工具,用来替代 mpirun,专门启动多卡 / 多机分布式训练,底层通信依然用 NCCL
- torchrun 专为 AI 训练设计,比 MPI 简单太多:
- 不需要装 OpenMPI
- 不需要复杂参数
- 自动识别单机多卡
- 自动分配 rank
- 自动处理多机 IP
- 支持弹性训练(节点挂了能恢复)
- PyTorch 原生支持,零配置