基础知识

整理各种面试中常见的八股问题,涵盖 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] 整数对象,全局复用
      • 这些整数赋值时不会新建对象,而是直接引用池内对象
      • 超过范围的整数不会缓存
    • 字符串驻留(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.ThreadPoolExecutor

      import 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.ProcessPoolExecutor

      from 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 代回收后存活的
      • 存活越久的对象,越不可能是垃圾
  • 弱引用 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.contextmanager

    • yield 之前 = __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(),上一轮迭代的梯度会累加到当前轮,导致梯度错误、模型无法收敛
  • 为什么要进行梯度裁剪?
    • 解决深度网络训练中的梯度爆炸问题(梯度过大导致参数更新失控、loss 震荡 / 发散)
    • 强制将梯度限制在设定阈值内,稳定训练过程,保证模型收敛
  • in-place 操作是什么?
    • 不创建新张量,直接在原内存地址上修改张量值的操作(变量名不变,值被覆盖)
    • x += 1、x.add_(1)(带_的都是 in-place)
    • 极易破坏计算图,导致反向传播报错,自动微分场景禁止使用
  • BF16 和 FP16 的区别?有什么风险?FP16 为什么更需要通过 scale 避免梯度下溢?
    • 对比

      特性FP16BF16
      位宽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 等转为静态图
  • 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)
  • 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 的成本
    • 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

      • 即 σ(x)=11+e−x\sigma (x)=\frac {1}{1+e^{-x}}
      • 输出 0~1,会梯度消失,几乎不再单独使用
      • 只用来做门控,不做特征激活
    • ReLU 最古老(2011)

      • 即 ReLU(x)=max⁡(0,x)ReLU (x)=\max (0,x)
      • 死亡神经元:一旦输入负数,输出 = 0,梯度 = 0,神经元永远不更新,直接 “死了”
      • 输出不是零中心化:导致梯度下降震荡
      • 负数信息完全丢失:信息损失大
    • SiLU(Swish)

      • 即 SiLU(x)=x×σ(x)SiLU (x)=x\times\sigma (x)
      • 正数:接近 x(线性)
      • 负数:平滑下落到 0(不是直接截断)
      • 处处可导,没有死神经元
    • GELU (2016 年,GPT、BERT 用)

      • 即 GELU(x)=x⋅Φ(x)GELU (x)=x\cdot\Phi (x),Φ(x)\Phi (x) 是标准正态分布的 CDF
      • 高斯误差线性单元,平滑、柔和、可导
      • 不会死亡神经元,训练更稳定
    • 门控线性单元的结构

      x ──→ linear1 → activation → gate
      x ──→ linear2 → value
      gate * value → output
    • SwiGLU(2020 年之后,LLaMA、所有大模型都用,门控线性单元)

      • 不是普通激活函数,是门控线性单元
      • 即 SwiGLU(x)=SiLU(Wx)⊗(Vx)SwiGLU (x) = SiLU (Wx) \otimes (Vx)
      • 门控机制,信息流动更精细,训练更稳、收敛更快
    • GeGLU(门控线性单元)

      • 即 GeGLU(x)=GELU(Wx)⊗(Vx)GeGLU (x)=GELU (Wx)\otimes (Vx)

优化器

  • 梯度下降优化器(SGD/Momentum/Adam/AdamW/Lion)的原理、区别?

    • 梯度下降优化器就是用来最小化损失函数、迭代更新模型参数的算法,核心思想是沿着损失函数梯度的反方向更新参数,一步步走到局部 / 全局最优
    • 本质:控制参数更新的步长与方向,让模型收敛更快、更稳、泛化更好
    • 梯度下降优化器本质都是沿着负梯度方向更新参数,区别在于是否用动量、是否做自适应学习率、是否对权重衰减正确处理、以及更新方式(一阶矩 / 二阶矩 / 符号更新)
    • 符号定义
      • gtg_t:当前这一步的梯度(方向 + 大小)
      • η\eta:学习率
  • SGD

    θt+1=θt−ηgt\theta_{t+1} = \theta_t - \eta g_t
    • 每次用单个样本 / 小批量计算梯度,实现简单
    • 梯度噪声大,收敛慢,容易在鞍点 / 局部最优徘徊
    • 而后续改进也主要是解决两个问题:
      • 让方向更稳,不乱抖 → 一阶动量做的
      • 让每个参数有自己合适的步长 → 二阶矩做的
  • SGD + Momentum

    vt=βvt−1+gtθt+1=θt−ηvtv_t = \beta v_{t-1} + g_t\\ \theta_{t+1} = \theta_t - \eta v_t
    • 梯度不是 “只看当前一步”,而是把最近几步的梯度加起来平均一下
    • 引入动量,累积历史梯度方向,能够让震荡方向被平滑掉,前进方向被加强
    • 减小梯度震荡,加速收敛,能冲出局部最优和平坦区域
  • RMSProp

    E[g2]t=β2 E[g2]t−1+(1−β2) gt2θt+1=θt−η gtE[g2]t+ϵE [g^2]_t = \beta_2 \, E [g^2]_{t-1} + (1-\beta_2)\, g_t^2 \\ \theta_{t+1} = \theta_t - \eta \,\frac {g_t}{\sqrt {E [g^2]_t + \epsilon}}
    • 对每个参数,记录它历史梯度的平方和,代表这个参数梯度 “大不大”
      • 梯度一直很大的参数 → 分母变大 → 步长缩小
      • 梯度一直很小的参数 → 分母变小 → 步长放大
    • 每个参数的学习率都不一样:
      • 稀疏特征、小梯度 → 学习率自动变大
      • 密集特征、大梯度 → 学习率自动变小
    • 通过加入二阶估计,自适应学习率,解决不同参数梯度尺度差异大的问题
    • 对每个参数自适应缩放梯度,适合稀疏梯度、非凸优化
  • Adam(自适应矩估计)

    • 结合 Momentum + RMSProp,维护一阶矩(梯度均值,动量)、二阶矩(梯度平方均值,自适应学习率)

    • 对一阶、二阶矩做偏差修正,解决初期偏置问题,每个参数自适应调整学习率

    • 收敛极快,对超参不敏感,可能泛化不如 SGD,容易过拟合

    • 梯度带 L2

      gt=∇L(θt)+λθtg_t = \nabla L (\theta_t) + \lambda \theta_t
    • 一阶矩估计(动量)

      mt=β1mt−1+(1−β1)gtm_t = \beta_1 m_{t-1} + (1-\beta_1) g_t
    • 二阶矩估计(RMSprop)

      vt=β2vt−1+(1−β2)gt2v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2
    • 偏差校正

      m^t=mt1−β1tv^t=vt1−β2t\hat {m}_t = \frac {m_t}{1-\beta_1^t}\\ \hat {v}_t = \frac {v_t}{1-\beta_2^t}
    • 参数更新

      θt+1=θt−η⋅m^tv^t+ϵ\theta_{t+1} = \theta_t - \eta \cdot \frac {\hat {m}_t}{\sqrt {\hat {v}_t}+\epsilon}
    • L2 项 λθt\lambda \theta_t 被包含在梯度里,一起被 Adam 缩放,正则强度被扭曲

  • Adam 为什么要做偏差矫正?

    • 常用 β1≈0.9\beta_1 \approx 0.9,β2≈0.999\beta_2 \approx 0.999,而因为 mtm_t、vtv_t 初始化为 0,在训练刚开始几步时,一阶、二阶矩估计会系统性偏小、偏向 0,导致更新幅度被低估

    • 偏差矫正就是把这个低估的部分 “补回来”,让前期训练更稳定

      • 以一阶矩为例,期望展开:

        E[mt]=(1−β1)∑i=1tβ1t−iE[gi]\mathbb {E}[m_t] = (1-\beta_1)\sum_{i=1}^t \beta_1^{t-i}\mathbb {E}[g_i]
      • 这是一个指数移动平均,真实梯度被系数 (1−β1)(1-\beta_1) 压制,当 t 很小时:

        E[mt]≪E[g]\mathbb {E}[m_t] \ll \mathbb {E}[g]
      • 二阶矩同理:

        E[vt]≪E[g2]\mathbb {E}[v_t] \ll \mathbb {E}[g^2]
    • 直接用原始值更新,会导致前期步长过小,训练启动很慢,容易陷入局部最优

    • 而偏差矫正就是除以 “累积权重和”,把被压低的期望拉回真实值,而由于 β1\beta_1 和 β2\beta_2 都小于 1,因此随着 tt 越大,其 tt 次方越趋近于 0,分母趋近于 1,校正放大效果几乎消失

  • AdamW

    • 原始 Adam 把权重衰减(Weight Decay)和学习率混在一起,AdamW 把两者解耦,权重衰减直接作用于参数,不通过梯度计算

    • 缓解 Adam 过拟合问题,泛化能力显著提升,现在训练 Transformer、大模型首选 AdamW

    • 只计算原始梯度

      gt=∇L(θt)g_t = \nabla L (\theta_t)
    • 动量 & 二阶矩(同 Adam)

      mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \\ v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2
    • 偏差校正(同 Adam)

      m^t=mt1−β1tv^t=vt1−β2t\hat {m}_t = \frac {m_t}{1-\beta_1^t} \\ \hat {v}_t = \frac {v_t}{1-\beta_2^t}
    • 关键:独立权重衰减 + 更新

      θt+1=θt−η(m^tv^t+ϵ)−ηλθt\theta_{t+1} = \theta_t - \eta\left (\frac {\hat {m}_t}{\sqrt {\hat {v}_t}+\epsilon} \right) - \eta\lambda \theta_t
    • 最后一项 −ηλθt-\eta\lambda \theta_t 就是独立权重衰减,不参与动量、二阶矩计算

  • Lion(EvoLved Sign Momentum)

    • 基于符号动量,不使用梯度本身,只用梯度的符号(sign(gt)\text{sign}(g_t))更新;内存更小,只维护一阶动量,无二阶矩

      ct=β1⋅mt−1+(1−β1)⋅gt(动量组合)mt=β2⋅mt−1+(1−β2)⋅gt(更新动量)θt+1=θt−η⋅(sign(ct)+λ⋅θt)\begin {align*} c_t &= \beta_1 \cdot m_{t-1} + (1-\beta_1) \cdot g_t \quad \text{(动量组合)} \\ m_t &= \beta_2 \cdot m_{t-1} + (1-\beta_2) \cdot g_t \quad \text{(更新动量)} \\ \theta_{t+1} &= \theta_t - \eta \cdot ({\text{sign}(c_t)} + \lambda \cdot \theta_t) \end {align*}
      • mtm_t:只存一个动量
      • sign(x)\text{sign}(x):符号函数 → 输出 +1 / 0 /-1
      • λ\lambda:解耦权重衰减(同 AdamW)
    • 用 sign () 代替自适应缩放

      • Adam:mtm_t、vtv_t → 每个参数步长不一样
      • Lion:sign(ct)\text{sign}(c_t) → 所有参数步长大小相同,只有方向不同
    • 为什么 sign () 反而更好?

      • 抗噪声:梯度噪声大时,sign()\text{sign}() 只看方向、不看大小 → 更稳
      • 步长均匀:不会出现某些参数步长爆炸、某些几乎不动
      • 计算更快:少了除法、开方、平方 → 速度快 10%~15%
    • 训练大模型、视觉模型效果 often 优于 AdamW,对学习率更敏感,需要精细调参

      • 必须调小学习率(η≈10−4\eta \approx 10^{-4} 而非 10−310^{-3})
      • 小模型 / 小 batch 有时不如 Adam 稳
      • 稀疏特征场景略弱(因为没有自适应缩放)
  • Muon(Momentum Orthogonal Newton)

    • 核心思想

      • Adam:把权重展平成向量,按单个参数逐元素更新,易出现方向冲突、冗余计算
      • Muon:把权重当成矩阵(如 Transformer 的 dmodel×dff),对动量做正交化,让更新方向 “彼此垂直、幅度一致”,消除冗余探索
      • 核心:让更新矩阵尽可能正交 → 防止各方向互相干扰、加速收敛
    • 对权重矩阵 W∈Rd×kW\in\mathbb {R}^{d\times k},动量矩阵 MtM_t 也是 d×kd\times k,每行代表一个参数向量的动量

      Mt=μ⋅Mt−1+gt(动量,矩阵形式)Ot=Newton-Schulz(Mt)≈(MtMt⊤)−1/2Mt(正交化)Wt+1=Wt−η⋅Ot−λ⋅Wt\begin {align*} M_t &= \mu \cdot M_{t-1} + g_t \quad \text{(动量,矩阵形式)} \\ O_t &= \text{Newton-Schulz}(M_t) \approx (M_t M_t^\top)^{-1/2} M_t \quad \text{(正交化)} \\ W_{t+1} &= W_t - \eta \cdot O_t - \lambda \cdot W_t \end {align*}
      • MtM_t:动量矩阵
      • Newton-Schulz:快速迭代求 正交近似(Ot⊤Ot≈IO_t^\top O_t\approx I),将动量矩阵转为最近正交矩阵(等价于 SVD 后将奇异值矩阵置为单位阵,仅保留方向)
      • 结果:更新方向互相垂直、不互相抵消,各方向均匀收敛、不易震荡、更快到全局最优
  • 大规模 LLM 如何用 Muon?

    • 混合优化:输入 / 输出层 / 偏置 / LayerNorm 用 AdamW,隐藏层用 Muon(加权重衰减 + 逐参数 LR 校准);配合 ZeRO-1 分布式,显存减半、通信可控
    • 稳定技巧:大模型加 MuonClip(QK-Clip),限制注意力头逻辑值,避免训练 “飙车失控”
  • AdamW 相比 Adam 的改进?

    • AdamW = 修正了 Adam 中 “权重衰减计算错误” 的版本
    • Adam 里的权重衰减(L2 正则)是在梯度更新之后加的,导致正则效果被学习率影响,变得不稳定
    • AdamW 改进
      • 把权重衰减独立出来,不跟梯度、动量混合

      • 直接对参数做衰减

        w=w−lr⋅(grad+λw)w=w-lr\cdot (grad+\lambda w)
      • 正则效果更稳定、泛化更好,不会因为学习率变化而失效

  • 正则化(L1/L2/Dropout)的作用?大模型中的正则化技巧?

    • 过拟合的本质之一就是模型用了太多无关特征、噪声特征去硬记训练集,而正则化就是给模型加约束,防止它过度拟合训练数据,提升泛化能力
    • L1 正则化在目标函数中加入权重的绝对值和
      • 梯度固定为 ±λ\pm\lambda,不管 w 多小,都持续往 0 拉,从而把大量不重要特征的权重直接压到 0,相当于自动扔掉没用 / 噪声特征,而只保留真正有用的特征,进而达到稀疏、特征选择的目的
      • L1 正则通过把噪声 / 无关特征的权重变成 0,模型只依赖少数强特征,模型结构变简单,自然就不容易过拟合,其梯度为 sign 函数
      • 适合要稀疏、要特征选择的场景
    • L2 正则化在损失函数中加入所有权重的平方和,其梯度即为参数本身
      • 在梯度下降时,相当于每次更新都先把权重朝 0 方向缩小一点,让模型权重不会过大、函数更平滑,从而抑制过拟合,提升泛化能力
      • 适合要稳定、平滑、防过拟合、好训练的场景
    • Dropout 从参数正则化转向到结构正则化,Dropout 并不是在目标函数中显式加项,而是在训练阶段随机破坏网络结构
    • 过拟合的一个重要原因是神经元之间互相依赖、互相配合去记忆噪声,而通过 Dropout,每次训练,网络结构都不一样,从而防止共适应
    • 其他正则化手段
      • 数据层面:增加训练数据的多样性、数据清洗 / 去噪
      • 模型 / 训练策略:早停(Early Stopping)、权重共享(Weight Sharing)、标签平滑(Label Smoothing)
  • 什么是标签平滑?

    • 标签平滑是一种正则化手段,将原本 one-hot 硬标签 [1,0,0...] 替换为软标签,把一小部分概率 ϵ\epsilon 均匀分给其他类别
    • 目的是防止模型对训练样本过度自信、过拟合,让模型学习更泛化的决策边界,同时让输出分布更平滑,提升鲁棒性和泛化能力,常用于分类、检测等任务
    • 传统 CV/NLP 分类任务中,固定类别数 C(1000、10000 类),一个样本只对应一个硬标签,模型容易对标签过度自信、过拟合,所以用标签平滑做正则,让分布软一点
    • 而大模型的训练其任务不是分类任务而是序列生成 / 语言建模,自回归预测下一个 token,每个位置都是一个多分类任务,在这种极端分类的情况下,正确 token 概率本来就很小,模型天然很难 "极端自信",标签平滑的正则效果微乎其微
    • 相反地,标签平滑会直接损害困惑度(PPL)与建模能力,因为 LLM 的核心目标是精准你和真实文本的分布,标签平滑反而把正确 token 的概率挖掉一块强制分给所有其他无关 token,使得模型不敢给正确词高分,从而使得困惑度变差
  • LR

    • 为什么必须 warmup?
      • 前期模型参数随机、不稳定,直接用大 LR 会震荡、不收敛甚至 NaN,而 Warmup 让学习率从极小慢慢升到最大值,保证训练稳定启动
    • 为什么必须 decay?
      • 后期模型接近收敛,需要减小步长,精细微调权重,防止在最优解附近震荡,提高最终精度
      • 同时也是一种隐式正则,缓解过拟合 ——LR 越来越小,权重更新幅度变小
    • LR 的 decay 策略有哪些?
      • 阶梯衰减

        lr=lrbase×γ⌊step/step_size⌋lr = lr_{base} \times \gamma^{\lfloor step /step\_size \rfloor}
      • cosine decay

        lr=0.5×lrmax×(1+cos⁡(TcurTmaxπ))lr = 0.5 \times lr_{max} \times \big (1 + \cos (\frac {T_{cur}}{T_{max}} \pi)\big)
      • 多项式衰减

        lr=lrbase×(1−TcurTmax)plr = lr_{base} \times (1 - \frac {T_{cur}}{T_{max}})^p
      • 指数衰减

        lr=lrbase×γsteplr = lr_{base} \times \gamma^{step}
      • inverse sqrt /rsqrt decay(倒数平方根)

        lr∝1steplr \propto \frac {1}{\sqrt {step}}
  • 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 → 训练变慢、容易陷入局部最优
    • 调太大 → 震荡、不收敛

其他

  • 交叉熵损失的计算细节?

    • 公式

      CrossEntropy(p,y)=−Σyilog⁡(softmax(pi))\text{CrossEntropy}(p,y)=-\Sigma y_i\log (\text{softmax}(p_i))
    • 第一步,先对 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?

    Precision=TPTP+FPRecall=TPTP+FNF1=2×Precision×RecallPrecision+Recall\text{Precision} = \frac {TP}{TP+FP}\\ \text{Recall} = \frac {TP}{TP+FN}\\ F1 = 2 \times \frac {\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}}
    • TP(真阳性):预测有,且确实对
    • FP(假阳性):预测有,但实际错 / 多余
    • FN(假阴性):实际有,但没预测到
    • Precision(精确率):预测出来的东西里,多少是对的
    • Recall(召回率):标准答案里的东西,找回了多少
    • F1 同时考虑精确率和召回率,避免只准不全或只全不准
  • 三种浮点格式:符号位 + 指数位 + 尾数位

    类型符号指数尾数总 bit特点
    FP32182332精度最高、范围大
    FP16151016精度还行,但范围小
    BF1618716范围和 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 原生支持,零配置
这篇文档有帮助吗?

On this page

页面信息