news 2026/10/27 19:27:36

PyTorch张量与GPU加速:设备迁移、显存管理与计时实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch张量与GPU加速:设备迁移、显存管理与计时实战

张量(Tensor)是 PyTorch 里所有计算的载体,GPU 加速(GPU Acceleration)则是让这个载体真正跑起来的那套机制。很多人学完 CPU 上的张量创建、切片、广播、矩阵乘,觉得挺顺,一到真机就懵了:device不匹配的报错、显存莫名其妙爆掉、明明换了显卡加速比却只有 1.2 倍、计时器测出来的时间是假的……这些问题在教程里往往一句话带过,实际干活时能耗掉你一整天。

我写这篇东西的出发点是:把“张量上 GPU”这件事从“知道要写 .cuda()”推进到“知道每一步为什么这么写、什么时候不该这么写”。它会讲清楚设备抽象是怎么设计的、数据迁移的三种写法差在哪、怎么测才算测准了、显存到底被谁吃了、训练循环里那几个参数为什么必须成对出现。适合刚上手 PyTorch 的同学,也适合已经能跑通模型、但没系统梳理过数据搬运成本的老手——尤其是那些被CUDA out of memory折磨过几次的人,看完应该能把排查顺序理清楚。

1. 先把“GPU 加速”想明白:它不是换个芯片那么简单

1.1 CPU 和 GPU 是两种性格完全不同的工人

刚接触的人容易有个误解,觉得 GPU 是“更快的 CPU”。这个理解偏差会直接影响你写代码的方式。CPU 是少数几个非常聪明的核心,主频高、分支预测强、缓存层级复杂,擅长处理“下一步做什么取决于上一步结果”这种逻辑密集的任务;GPU 是几千个相对简单的核心排成阵列,每个核心只会做加减乘除,但它能同一时刻让几千个核心干同一件事。

张量运算恰好是后者的天菜。矩阵乘法、逐元素加法、卷积、归一化,拆开看全是“对每个元素执行相同的公式”,元素之间几乎没有依赖。这种结构在 CPU 上只能一个核一个核地啃,在 GPU 上可以一次性铺开。所以同一个 4096×4096 的矩阵乘,CPU 上可能是秒级,GPU 上能压到几十毫秒——差的是两个数量级。

但这个优势有前提:数据必须已经在显存里。一旦涉及“把数据从内存搬到显存”,性质就完全变了,搬运本身成了瓶颈。这就是下一节要讲的事。

1.2 搬运的代价:为什么频繁往返会把加速比吃干净

设备之间的数据通道带宽是有限的。以常见的桌面平台为例,主板上的高速串行通道理论带宽在十几到三十几 GB/s 量级,实际有效吞吐还要打折;而显卡内部显存到计算单元的带宽是几百 GB/s 到数 TB/s 量级。两者差了一到两个数量级。

这意味着什么?我给你算一笔账。一个 1024×1024 的 float32 张量,元素数约 105 万,每个元素 4 字节,总共约 4 MB。走一次主机到设备,按 10 GB/s 的有效带宽算,大约 0.4 毫秒;设备回主机再来 0.4 毫秒。往返接近 1 毫秒。而 1024 阶矩阵乘在入门级独显上大概 0.4 毫秒。也就是说,如果你写完就算、算完立刻拿回 CPU 看结果,搬运时间比计算时间还长,加速比直接被腰斩。

更糟的是“循环里反复搬”。我见过不少新手代码长这样:在 for 循环里每次x = x.cuda()、算一步、x = x.cpu()、用 numpy 处理一下、再搬回去。这种写法跑起来可能比纯 CPU 还慢,然后就开始怀疑是不是显卡不行。实际是自己把最贵的那步放进了最热的循环里。

记住一条:设备间搬运是“批量做才划算”的操作,你要做的是把数据一次性搬上去,让尽量多的计算在显存里连续完成,最后再统一搬回来。

1.3 什么情况下不值得上 GPU

这条经验很多人不爱听,但确实能省你不少时间。下面几种情况,CPU 反而是更优解:

  • 张量很小。比如几百乘几百的矩阵、几千个元素的向量。计算量小到搬运开销完全盖过收益,而且每次 kernel 启动还有固定开销(微秒级),小算子堆多了开销很可观。
  • 逻辑分支密集。大量 if/else、动态索引、变长循环,这类代码在 GPU 上会严重串行化,甚至触发一次次隐式的设备同步。
  • 数据预处理阶段。解码图片、分词、数据增强这类操作,CPU 多进程做得又稳又便宜,硬塞给 GPU 往往得不偿失。
  • 显存本身不够。模型放不下就是放不下,硬上只会换来无穷无尽的 OOM。

判断标准很朴素:先用 CPU 跑通、跑对,再看耗时热点在哪。热点是连续的大规模数值计算,才轮到 GPU 上场。上来就全链路 GPU 化,通常是把调试难度放大三倍。

2. PyTorch 的设备抽象:device、cuda 与张量的归属

2.1 torch.device 这套设计到底解决了什么问题

PyTorch 里的每个张量都带一个device属性,标明它住在哪。这个设计的关键价值是“把设备选择变成一个可传递的变量”,而不是散落在各处的硬编码。你可以写一套代码,在有卡和没卡的环境里都跑得动。

import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(device) # cuda:0 或 cpu x = torch.randn(3, 4, device=device) print(x.device)

torch.device接受的写法有几种,含义差别不大但用途不同:

写法含义适用场景
torch.device("cpu")主机内存兜底分支
torch.device("cuda")默认卡(通常是 0 号)单卡场景
torch.device("cuda:0")指定 0 号卡多卡明确指定
torch.device("cuda", 1)同上,另一种写法参数化生成
"cuda"字符串大多数 API 会自动转换快速脚本

实践中的做法是把device定义在配置层,模型、数据、损失函数全部引用同一个变量。不要在一个文件里出现x.cuda()、另一个文件里出现.to("cuda:0")、第三个地方又是.to(torch.device('cuda', 0))。看着都对,一旦要切到 CPU 调试或者换卡,你得改十几个地方,还容易漏。

2.2 三种迁移写法的差异和踩坑点

把张量搬到设备上,常见三种写法,行为不完全一样:

x = torch.randn(3, 4) # 写法一:硬编码,没卡直接报错 x1 = x.cuda() # 写法二:字符串简写 x2 = x.to("cuda") # 写法三:走统一的 device 变量(推荐) x3 = x.to(device)

写法一的坑在于不可移植。你本地有卡,跑到一台只有 CPU 的机器上做小规模验证,代码第一行就崩,报的还是AssertionError: Torch not compiled with CUDA enabled这种让人摸不着头脑的信息。写法二的问题是分散,一旦有十几处,改设备就得全局搜。

真正需要警惕的是返回值这件事。x.to(device)不是原地修改,它返回一个新张量。很多人写:

x.to(device) y = x @ w # 报错:x 还在 cpu

这里x压根没变,只是产生了一个被丢弃的副本。正确写法是x = x.to(device)。这个坑之所以常见,是因为nn.Module.to()是原地修改的——模型的参数会被就地替换成设备上的版本,方法返回self只是方便链式调用。两个.to()语义不同,很容易形成肌肉记忆错乱。

一个小技巧:如果你不确定某个张量在哪,直接print(t.device)或者assert t.device == device。在调试阶段加一行断言,比事后翻报错快得多。

2.3 dtype、contiguous 与“隐性复制”

设备迁移经常和类型转换一起做,to()支持同时指定:

x = x.to(device=device, dtype=torch.float16)

注意这里如果device和dtype都和原来一致,to()会直接返回原张量本身,不复制。这个行为对内存友好,但也意味着你不能靠to()强制拿一份拷贝——需要副本请用clone()。

另一个容易忽略的点是内存布局。GPU 上的某些算子(尤其是底层调用高度优化库的)要求输入是连续内存。从 CPU 搬上来的张量如果经过转置、切片等操作,stride 可能不连续,直接喂给这些算子会报view size is not compatible with input tensor's size and stride。解决办法:

x = x.to(device).contiguous()

contiguous()会按行优先重新排布一份连续副本,代价是一次额外拷贝。在数据加载阶段做一次,比在热路径里反复触发划算。

还有一类隐性复制值得说:torch.tensor(某个numpy数组)会复制数据,torch.from_numpy(arr)则与 numpy 共享内存。后者转到 GPU 之后共享关系自然断开,但如果你的流程是“numpy 处理 → from_numpy → 上 GPU”,中间省掉的那次拷贝在小数据上无所谓,在 GB 级数据上就是实打实的时间。

3. 从零跑通一个能说明问题的 GPU 对照实验

3.1 环境自检:先确认你的卡真的被用上了

动手之前先做一次体检。很多“GPU 没加速”的问题,根源是代码压根跑在 CPU 上,只是没人发现。

import torch print("torch 版本:", torch.__version__) print("CUDA 可用:", torch.cuda.is_available()) print("设备数量:", torch.cuda.device_count()) if torch.cuda.is_available(): print("设备名:", torch.cuda.get_device_name(0)) print("计算能力:", torch.cuda.get_device_capability(0)) print("显存总量:", round(torch.cuda.get_device_properties(0).total_memory / 1024**3, 2), "GB")

如果is_available()返回 False,按这个顺序查:

  1. 驱动是否正常装上。系统自带的显卡状态查询命令能列出设备信息,说明驱动层是通的。
  2. 装的是不是 CPU 版本的 PyTorch。torch.__version__里带+cpu后缀就说明装错了包,需要重装对应的构建版本。
  3. 框架版本和驱动版本是否匹配。这类问题报错信息通常很直白,比如提示构建版本不兼容。
  4. 在容器里跑的话,宿主机有没有把设备透传进容器。

版本对齐这件事我要多嘴一句:torch.version.cuda显示的是编译时链接的运行时版本,nvidia-smi右上角显示的是驱动支持的最高版本。前者的主版本号不能高于后者,否则会出现“能 import 但一用就报错”的诡异状态。

3.2 计时方法不对,测出来的数全是假的

GPU 计算是异步的。你在 Python 里调用一个 kernel,函数立刻返回,真正的计算还在设备队列里排队。所以用time.time()包一段 GPU 代码,测到的往往只是“把任务塞进队列”的时间,几微秒而已。

正确的做法有两种。简单的是在计时前后加同步:

import time torch.cuda.synchronize() t0 = time.perf_counter() y = a @ b torch.cuda.synchronize() t1 = time.perf_counter() print(f"{(t1 - t0) * 1000:.2f} ms")

更精确的是用 CUDA 事件,它直接在设备侧打时间戳,不受主机侧调度抖动影响:

def bench(fn, warmup=10, iters=50): for _ in range(warmup): fn() torch.cuda.synchronize() start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() for _ in range(iters): fn() end.record() torch.cuda.synchronize() return start.elapsed_time(end) / iters # 毫秒

这里warmup不是可选项。第一次调用某个算子时,框架要做内核加载、内存池初始化、算法选择等一堆准备工作,耗时可能是稳态的几十倍。不预热直接测,你会得到一个完全失真的数字。

3.3 一份可以直接抄的对照实验

下面这段代码把“纯计算”和“端到端(含搬运)”分开测,这是理解加速比的关键:

import torch, time device = torch.device("cuda" if torch.cuda.is_available() else "cpu") def bench_cpu(n, iters=20): a = torch.randn(n, n) b = torch.randn(n, n) torch.set_num_threads(8) for _ in range(3): a @ b t0 = time.perf_counter() for _ in range(iters): a @ b return (time.perf_counter() - t0) / iters * 1000 def bench_gpu_compute(n, iters=50): a = torch.randn(n, n, device=device) b = torch.randn(n, n, device=device) for _ in range(10): a @ b torch.cuda.synchronize() s, e = torch.cuda.Event(True), torch.cuda.Event(True) s.record() for _ in range(iters): a @ b e.record() torch.cuda.synchronize() return s.elapsed_time(e) / iters def bench_gpu_end2end(n, iters=20): a_cpu = torch.randn(n, n) b_cpu = torch.randn(n, n) for _ in range(5): (a_cpu.to(device) @ b_cpu.to(device)).to("cpu") torch.cuda.synchronize() t0 = time.perf_counter() for _ in range(iters): c = (a_cpu.to(device) @ b_cpu.to(device)).to("cpu") torch.cuda.synchronize() return (time.perf_counter() - t0) / iters * 1000

我在一台普通台式机(8 核处理器 + 一张入门级独显,走的是 PCIe 4.0 x16 通道)上跑出来的结果,趋势大致如下。这些数字只反映量级关系,换硬件会变,别拿去当基准:

方阵规模CPU 耗时GPU 纯计算GPU 端到端(含往返搬运)端到端加速比
64×640.02 ms0.01 ms0.05 ms约 0.4 倍(更慢)
256×2560.4 ms0.03 ms0.35 ms约 1.1 倍(持平)
1024×102420 ms0.4 ms1.3 ms约 15 倍
4096×40961500 ms35 ms50 ms约 30 倍

这张表最值得看的是第二列和第四列的差距。在 256 阶这个尺度上,纯计算 GPU 快 13 倍,但算上搬运之后只剩 1.1 倍。如果你只看纯计算数字,会以为自己的链路已经优化到位了,实际收益被搬运吃了个精光。

3.4 结果怎么读:三个容易被误判的情况

第一种误判是拿单次结果做结论。单次测量受系统调度、后台任务、显卡降频影响很大,抖动能有 20% 以上。至少要跑 20 次取平均,再跑几组不同规模,看趋势。

第二种误判是忘了 CPU 侧的多线程设置。默认线程数在有些环境里被设成 1,CPU 基准成绩被严重低估,显得 GPU 快得离谱。torch.set_num_threads(8)这一行在对照实验里不能省。

第三种误判是把不同的数据类型放在一起比。float32 和 float16 的计算吞吐本来就差一倍甚至更多,一边用 fp32 一边用 fp16 得出结论,没有意义。要比较就锁死 dtype。

还有一点:4096 阶那行的端到端时间 50 ms,主要花在哪?67 MB 的数据搬上去 7 ms,算 35 ms,结果 67 MB 搬回来 7 ms。这里计算占大头。但如果你做的是“小模型 + 大数据”,比例会反过来,搬运可能占到七成以上。这时候优化方向就不是换卡,而是减少往返次数。

4. 训练循环里的 GPU 加速:数据流水线与显存管理

4.1 pin_memory + non_blocking 这一对参数为什么必须成对出现

训练时最常见的搬运方式是每个 batch 从主机内存拷到显存。默认情况下这个拷贝是同步的:主机发起拷贝,然后阻塞等待,等拷贝完了才能继续。这段时间 GPU 是闲着的。

PyTorch 给了两个配套的开关。DataLoader的pin_memory=True会把 batch 放进页锁定内存,这种内存不会被操作系统换出到磁盘,GPU 的直存引擎可以直接访问,省掉一次中间拷贝;non_blocking=True让拷贝调用立刻返回,主机继续去准备下一个 batch,拷贝在后台完成。

from torch.utils.data import DataLoader loader = DataLoader( dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True, persistent_workers=True, ) for x, y in loader: x = x.to(device, non_blocking=True) y = y.to(device, non_blocking=True) # 前向、反向照常写

几个必须知道的细节:

  • non_blocking=True只有在源数据位于页锁定内存时才真正异步。普通内存上的张量,即使传了 True 也会退化成同步拷贝,白写。
  • pin_memory=True会占用不可换出的物理内存。num_workers 多、batch 大的时候可能吃掉几个 GB,机器同时开别的程序容易卡。别把值设得太大。
  • 异步拷贝之后,如果立刻在主机侧读取该张量的内容(比如打印一个数),框架会隐式同步,异步的收益就没了。别在热路径里干这种事。
  • persistent_workers=True能省掉每个 epoch 重建进程的开销,配合pin_memory用效果更明显,但要注意数据集里如果有带状态的随机数生成器,得手动处理种子。

实测下来的感受是:在图像和文本任务上,这一对开关能把数据供给的等待时间压掉三到五成,具体取决于你的预处理有多重。如果你的数据加载本来就很快,收益就不明显——先 profile 再决定要不要开,别盲目抄配置。

4.2 显存不是硬盘:缓存分配器与 empty_cache 的正确用法

很多人第一次遇到 OOM 的第一反应是加torch.cuda.empty_cache(),加在循环里,然后发现又慢又没用。要理解为什么,得知道 PyTorch 的显存管理方式。

PyTorch 不会每次张量分配都向驱动申请,它有一个缓存分配器。第一次申请一大块显存,之后需要小张量就从这块里切,用完还回池子而不是还给驱动。好处是分配极快,坏处是memory_reserved会一直保持高位,看起来像“泄漏”。

三个查询接口要分清楚:

print(torch.cuda.memory_allocated() / 1024**2, "MB") # 当前张量实际占用 print(torch.cuda.memory_reserved() / 1024**2, "MB") # 分配器持有的总池子 print(torch.cuda.max_memory_allocated() / 1024**2, "MB") # 峰值,用来定 batch size

memory_allocated高才是真的用了这么多;memory_reserved高只是池子大,池子里的空闲块可以被后续分配复用。empty_cache()释放的是池子里的空闲块,正在用的张量它碰不了。所以在循环里调它,等于每隔几步就把池子清空、下一步再重新申请,白白增加开销,还容易造成碎片。

它真正该用的场景只有两个:一是你想给另一个进程腾地方,二是你刚删掉一个大模型、想确认显存确实还回去了。日常训练不要碰它。

4.3 batch size 怎么估:一个能落地的算法

显存估算这件事,与其瞎试,不如按公式推一遍再验证。以 Adam 优化器 + float32 训练为例,显存主要由四部分构成:

组成计算方式说明
参数P × 4 BP 是参数个数
梯度P × 4 B与参数同规模
优化器状态P × 8 BAdam 的一阶、二阶动量各 4 B
激活值与 batch、序列长度、层数相关最难估,通常是大头

固定部分好算:一个 1000 万参数的模型,参数 40 MB,梯度 40 MB,优化器状态 80 MB,加起来 160 MB,这部分和你 batch 设多大无关。

激活值难算,是因为它正比于 batch size,而且前向过程中间结果要留着给反向用。经验上,卷积网络里激活值占用的量级可以用“batch_size × 单个样本的中间特征总元素数 × 4 B”粗估,再乘一个 1.3 到 1.5 的系数应对碎片和临时缓冲。更实用的办法是:先用一个肯定能跑通的小 batch 跑一步,读max_memory_allocated,再按线性关系外推,留 20% 余量。

torch.cuda.reset_peak_memory_stats() # ... 跑一步完整的 forward + backward + step ... peak = torch.cuda.max_memory_allocated() / 1024**3 print(f"单步峰值 {peak:.2f} GB,可用 {torch.cuda.get_device_properties(0).total_memory/1024**3:.2f} GB")

注意:max_memory_allocated统计的是当前进程,别的程序占用的显存看不到。估完了还是 OOM,先用状态查询命令看一眼卡上是不是有别的进程。

另外提醒一句,模型里如果有大量中间结果被存进 Python 列表、字典或者被闭包捕获,那部分显存不会释放,会随 step 数线性增长。这种情况只要 batch 一小就能暴露出来:batch 减半后显存还是越跑越高,基本可以确定是泄漏,不是配置问题。

4.4 混合精度:一次把显存和带宽都省下来

如果显存卡在临界点,混合精度是性价比最高的一招。核心思路是前向和反向用 float16 算,主权重和优化器状态保持 float32,避免梯度下溢。代码骨架:

scaler = torch.amp.GradScaler("cuda") for x, y in loader: x = x.to(device, non_blocking=True) y = y.to(device, non_blocking=True) optimizer.zero_grad(set_to_none=True) with torch.amp.autocast("cuda", dtype=torch.float16): out = model(x) loss = criterion(out, y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

GradScaler干的事是给损失乘一个动态系数,把 float16 表示不了的小梯度放大,更新前再除回来。不写这一步,训练很容易在某几个 step 之后梯度全变 0,loss 卡住不动,而且不报错——这是混合精度最阴的坑。

版本上有差异:老版本写法是torch.cuda.amp.autocast和torch.cuda.amp.GradScaler,新版本统一成了torch.amp.*并强制传设备参数。抄代码前先看一眼自己的版本,报TypeError: __init__() missing 1 required positional argument基本都是这个原因。

收益方面,显存能省三到四成,吞吐提升常见在 1.5 到 2 倍。但要注意:不是所有算子都适合半精度。数值范围敏感的损失(比如涉及 exp、log 的)、小 batch 的批归一化统计,都可能出问题。第一次上混合精度,务必加一段小规模对照,跑几百步看 loss 曲线和 fp32 基线是否吻合。

5. 常见报错与排查实录

5.1 CUDA out of memory:按顺序排查,别上来就改 batch

OOM 是最常见的报错,也是最容易查错的。我看到的大部分情况,第一反应都是“减 batch”,减了发现还爆,就慌了。按下面的顺序走,命中率很高:

顺序检查项判断依据处理方式
1卡上是否被别的进程占着状态查询能看到其他进程结束占用进程或换卡
2验证/测试阶段是否漏了no_grad训练正常,验证爆显存用with torch.no_grad():包住
3是否有张量被意外持有batch 减半后仍逐步上涨用.item()取标量,别存 loss 张量
4batch 本身太大峰值稳定但不留余量减半试,或开梯度累积
5内存碎片总量够但分配失败重启进程,必要时设分配器配置
6显存足够但报错在特定算子单步峰值有尖峰拆分大算子,分块计算

重点说第 3 条,因为它最隐蔽。下面这种写法非常常见:

losses = [] for x, y in loader: loss = criterion(model(x), y) losses.append(loss) # 每个 loss 都带着整个计算图! loss.backward()

loss是一个带grad_fn的张量,它的计算图引用着中间激活值。你把它存进列表,等于把每一步的计算图都钉在显存里,step 越多占用越高,最后必然爆。改法是losses.append(loss.item()),只留一个 float。同理,日志里打印指标一律用.item()。

第 2 条也值得展开。验证阶段只做前向,但如果你忘了no_grad,框架会默默地把整条计算图建起来,显存占用和训练时差不多,甚至更高(因为 batch 往往更大)。加上@torch.no_grad()装饰器或者with上下文,显存立刻降下来,速度也能快两成。

5.2 device mismatch:把设备当成一个贯穿全局的约定

典型的报错长这样:

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

这句话的字面意思很清楚,难的是定位是哪一个张量没跟上。常见来源我按出现频率排一下:

  • 模型搬了,数据没搬。model.to(device)写了,x、y忘了。
  • 损失函数里混进了 CPU 张量。比如手动算了个权重矩阵、从 numpy 转过来的 mask、torch.tensor(某个数)没指定 device。
  • 创建张量时没给 device。torch.zeros(...)、torch.arange(...)默认在 CPU,写的时候顺手加device=device。
  • 从 checkpoint 加载时没指定映射。torch.load(path)会把张量还原到保存时的设备上,用torch.load(path, map_location=device)一次解决。
  • 多个模块设备不一致。主模型在 0 号卡,某个手工创建的辅助模块还在 CPU。

定位技巧:在报错的那一行前面,把所有参与运算的张量设备打印一遍。

print([t.device for t in (x, y, w, b)])

如果是模型内部报错,从模型的第一个参数取设备,和输入比一比:

model_device = next(model.parameters()).device print(model_device, x.device)

根治的办法是让所有创建张量的地方都显式带device,并且用一个项目级的device变量。这看起来啰嗦,但它把“设备”从隐式约定变成显式契约,后面加功能不容易出岔子。

5.3 关于.to()与优化器顺序的一个历史包袱

网上流传一个说法:“model.to(device)必须在创建优化器之前,否则优化器会失效”。这个说法来自很老的版本,当时.cuda()会重建参数对象。现在的实现是原地替换参数的存储,优化器的引用仍然有效,顺序无所谓。

但有一种情况确实要重建优化器:你写了model = model.to(device)这种重新赋值。如果to()返回的是同一对象,那没问题;但如果你自己写了model = MyModel().to(device)而优化器是基于旧对象建的,那优化器里持有的参数引用和模型里的新参数就对不上了,表现为训练完全不收敛、loss 一动不动,还不报错。

稳妥写法:先model = MyModel().to(device),再optimizer = torch.optim.Adam(model.parameters(), lr=...)。这个顺序在任何版本下都不会出问题,别为了省一行去赌。

同样的道理适用于学习率调度器、EMA 之类的辅助对象,它们都持有参数引用,创建时机放在模型彻底就位之后。

5.4 一份贴在显示器边上的速查表

现象最可能的原因快速验证处理
加速比只有 1.x 倍测的是端到端,搬运占大头分别测纯计算和端到端增大批量,减少往返
计时结果只有几微秒异步未同步加torch.cuda.synchronize()用 CUDA Event 计时
第一次调用特别慢内核加载、池初始化对比首次和第二次加预热轮次
显存持续增长计算图被持有 / 指标存了张量看memory_allocated曲线用.item(),加no_grad
总量够但分配失败碎片化重启后立即跑是否正常重启进程,调 batch
empty_cache后更慢池子被反复清空注释掉对比移除循环内的调用
混合精度 loss 卡住梯度下溢关掉 autocast 对比用 GradScaler 正确缩放
非连续张量报 stride 错转置/切片后直接送算子打印is_contiguous()加.contiguous()
加载模型后全在 CPUtorch.load默认还原原设备打印参数设备加map_location

最后再分享一个我一直在用的习惯:在训练脚本入口处加一段设备自检,把设备类型、名称、显存总量、当前进程已用显存打印出来,同时assert一下device.type和你预期的是否一致。这段代码不到十行,但它能在你跑了两小时发现结果不对的时候,第一时间告诉你“兄弟,你其实一直在 CPU 上跑”。踩过两次这种坑之后,我把它做成了所有项目的模板。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 19:25:12

SRS不是文档而是需求包:结构化需求规约与可验证性工程实践

简介:这份DOC文档面向软件需求分析人员、项目经理及测试与QA团队,提供IBM软件需求规约的格式模板要求与案例参考,用于解决SRS结构不清、需求收集与组织不规范等问题。文档共1个DOC文件,约150KB,篇幅紧凑,便…

作者头像 李华
网站建设 2026/10/27 19:23:54

数字图像处理中的光圈模糊技术详解

1. 光圈模糊的数字图像处理实现在摄影和图像处理领域,光圈模糊是一种模拟真实相机光圈特性的重要技术。它能够为数字图像添加自然的景深效果,让主体突出而背景柔和,这种效果在专业摄影和影视后期中应用广泛。不同于简单的高斯模糊&#xff0c…

作者头像 李华
网站建设 2026/10/27 19:20:43

智能安防视频分析:轻量化AI与GB28181协议优化实践

1. 项目背景与需求分析在公共安全领域,数字化安防体系正经历从"看得见"向"看得懂"的智能化转型。传统视频监控系统存在三大痛点:海量视频数据人工筛查效率低下、异构设备接入标准不统一、后端分析服务器负载过重。某市公安局的实战数…

作者头像 李华
网站建设 2026/10/27 19:20:38

复合材料壳单元求解器开发与优化实践

1. 项目背景与核心价值复合材料壳结构在现代工程中的应用越来越广泛,从航空航天到风电叶片,这类结构因其优异的比强度和刚度特性备受青睐。但复合材料的各向异性特性给结构分析带来了巨大挑战,传统的解析方法往往难以准确预测其力学行为。这正…

作者头像 李华
网站建设 2026/10/27 19:20:03

网页元素编辑技巧与性能优化实战指南

1. 网页元素编辑基础概念网页元素编辑是前端开发中最基础也最核心的技能之一。简单来说,它指的是通过代码或工具对网页中的各个组成部分进行修改、调整或重新设计的过程。一个典型的网页由HTML元素构成骨架,CSS赋予样式,JavaScript添加交互行…

作者头像 李华
网站建设 2026/10/27 19:19:33

MySQL数据库实验五:SQL脚本、触发器、存储过程与事务并发实战

简介:这份资源面向西北工业大学软件学院数据库课程学习者,聚焦实验五的E-Commerce数据库ER模式设计任务,适合正在完成实验、复习概念模型建模或需要对照示例查漏补缺的同学。压缩包共20个文件,约282KB,以14个GIF操作演…

作者头像 李华