PyTorch张量创建:torch.Tensor与torch.empty的深层区别与踩坑指南

发布时间:2026/10/2 9:44:33
PyTorch张量创建:torch.Tensor与torch.empty的深层区别与踩坑指南
1. 从一个诡异输出说起两种创建方法的第一眼区别有次我在调试一个图像预处理流程随手写了句x torch.Tensor(3, 4)结果打印出来是这样的import torch x torch.Tensor(3, 4) print(x)输出长这样tensor([[ 4.3701e10, 6.2543e-39, 1.4197e03, 3.9562e-42], [ 7.1732e10, 7.3378e-39, 5.6052e-45, 3.9565e-42], [ 3.9561e-42, 1.7194e03, 3.9562e-42, 4.3701e10]])第一次遇到这种情况的人多半会觉得“我是不是把模型权重给打出来了”或者怀疑机器出了问题。其实不是这只是torch.Tensor()在按尺寸创建张量时压根没有对内存里的数据做任何处理。换句话说你拿到的是一个“占着茅坑不拉屎”的容器里面是随机残留的旧数据。同样的现象torch.empty(3, 4)也会出现y torch.empty(3, 4) print(y)输出的值同样是随机的、不可预测的。从这一点来看torch.Tensor(3, 4)和torch.empty(3, 4)确实有非常相似的一面但它们在行为细节、使用惯用法和历史沿革上又有不小的区别。这篇文章我不打算只罗列 API 文档而是把两种方法在内存分配、默认数据类型、数据初始化、GPU 场景和深度学习训练中的实际影响都串起来讲一遍附带能直接复现的代码和踩坑经验。不管你是刚入门 PyTorch 的新人还是已经在项目里跑过不少模型的老手我建议把这两种创建方法的核心差异搞清楚。因为很多看似莫名其妙的nan、inf和 loss 不下降问题根源就藏在创建张量时那一下“没初始化”上面。2. torch.Tensor() 到底是函数还是类双重身份引发的常见误解2.1 类名和构造器混在一起导致行为不一致PyTorch 里的torch.Tensor首先是一个类是所有张量对象的类型。比如你执行torch.zeros(2, 2)返回对象的类型就是torch.Tensor。但torch.Tensor又可以直接像函数一样被调用比如torch.Tensor(2, 2)。这种“类 构造器”的双重身份让很多初学者一开始就犯迷糊。更麻烦的是torch.Tensor()在不同参数下表现完全不一样不传参数torch.Tensor()返回一个 0 维张量也叫标量张量。传一个或多个整数torch.Tensor(2, 3)返回形状为(2, 3)的未初始化张量。传一个列表或数组torch.Tensor([1, 2, 3])会把这个数据转成张量。传另一个张量torch.Tensor(existing_tensor)会返回一个和原张量共享内存的新张量。我第一次意识到这里有问题是在代码评审时看到同事写torch.Tensor(np.array([...]))来把 NumPy 数组转成 PyTorch 张量。这个写法本身能跑但它绕了一圈默认 dtype 是torch.float32而且如果传入数据本身是int64还会发生类型转换。更关键的是这种写法在代码里非常容易跟torch.Tensor(3, 4)这种“按尺寸创建”的用法混在一起时间一长读代码的人根本分不清你到底是想创建形状还是想转换数据。2.2 按尺寸创建时torch.Tensor 与 torch.empty 的等价关系torch.Tensor(3, 4)这种用法本质上就是在做torch.empty(3, 4)做的事。两者都会分配内存但不会初始化内存中的值。官方文档里也明确写了torch.Tensor()构造函数在传入尺寸参数时规则和torch.empty()保持一致。但这里有个微妙的差异torch.Tensor()是默认张量类型torch.FloatTensor的别名而torch.empty()是一个独立的函数支持通过dtype、device、layout等参数创建任意类型的张量。例如# torch.Tensor 方式无法直接指定 dtype只能用默认 float32 a torch.Tensor(2, 2) # torch.empty 方式可以明确指定 dtype 和 device b torch.empty(2, 2, dtypetorch.float64, devicecuda:0)如果你在 GPU 服务器上跑了混合精度训练想创建一个半精度的临时张量用torch.Tensor()就比较别扭因为它没有dtype参数你需要先创建torch.FloatTensor再转半精度或者干脆用torch.empty(..., dtypetorch.float16)。这也是我在实际项目中更倾向用torch.empty()的原因之一——它把“创建未初始化张量”这个语义表达得更直接、更可控。2.3 传入张量时的内存共享陷阱torch.Tensor()另外一个容易踩坑的地方是把一个已有张量传进去。很多人以为这等价于torch.tensor(x)那样的拷贝操作其实不然x torch.arange(5) y torch.Tensor(x) y[0] 100 print(x) # tensor([100, 1, 2, 3, 4])看到了吗x的值被改了。这说明y和x共享了底层存储。官方文档里明确提到torch.Tensor(existing_tensor)会创建一个与给定张量共享相同底层元素的新张量。如果你的本意是复制一份数据应该使用torch.tensor(x)或x.clone()。这种共享内存在某些场景下其实是有意为之的比如你想在不复制数据的前提下以另一个形状或视角去操作同一个底层存储。但问题是torch.Tensor(x)这个写法太隐蔽了读代码的人很难一眼看出它是在共享引用而不是复制数据。比较稳妥的做法是初始化数据用torch.tensor()复制张量用.clone()创建同形状的新张量用torch.empty_like()或torch.zeros_like()。这样代码意图清楚也避免在不知情的情况下改坏原始数据。3. torch.empty() 的设计原理与实战价值3.1 empty 是“不初始化”不是“没有数据”torch.empty()这个名字其实有一定的误导性。从名字来看它像是一个“空”张量好像里面没有任何元素。实际上它返回的张量有完整的形状和内存空间只是里面存的是未定义的值。你可以把它理解成租房时上一任房客留下的东西——房子是你的了但角落里可能还有别人不要的旧报纸、破拖鞋。由于未初始化的内存内容完全取决于操作系统和之前使用这块内存的程序所以每次运行torch.empty()打印出来的值都可能不一样。同一台机器上第一次可能是4.3701e10第二次可能变成-7.2834e05。这不是随机的随机数生成器而是纯内存残留。需要特别注意的是很多人在调试时看到torch.empty()的打印结果里有nan或inf就以为是计算过程出错了。如果这个张量是刚empty()出来的那这些异常值很可能只是内存残留不是真正的数学错误。3.2 不初始化的性能红利到底有多大既然empty()不做初始化那它自然比torch.zeros()少了一步“把所有元素写成 0”的操作。在张量比较小的时候这个差距几乎感觉不到。但张量一大差别就非常明显。我做过一个简单的性能实验在一张 V100 上分别创建 1 亿个浮点数的torch.empty()和torch.zeros()前者耗时大约几毫秒后者通常要多一个数量级以上。因为写 0 这个操作需要遍历整块显存而empty()只是向分配器申请了一块地址真正触碰内存的时间被推迟到了后续写入时。这就是为什么很多高性能推理框架在处理 KV Cache、PageAttention 这类场景时会优先用empty()或empty_like()来分配初始内存。它们知道这块内存马上就会被完整覆盖不需要预先清零。如果你在这类场景里傻乎乎地用torch.zeros()等于白白浪费了一次全量内存写入的带宽。但要注意性能红利只有在内存会被完整、确定地覆盖时才划算。如果分配了empty()之后只有部分位置被写入另一半位置还残留着旧数据那这些残留值一旦被参与运算就可能污染结果。3.3 empty_like 与 memory_format从形状复制到内存布局控制在实际训练代码里我经常用的是torch.empty_like()它比torch.empty()更方便因为你不用手动传形状、dtype、device 这些信息直接从参考张量上继承hidden torch.randn(4, 8) buffer torch.empty_like(hidden)torch.empty_like()还会自动继承参考张量的内存布局比如是否使用了channels_last格式。这在卷积神经网络里尤其重要。如果你用torch.empty()手动指定形状默认得到的布局是torch.contiguous_format也就是最常规的行优先连续布局但很多模型为了加速卷积把权重或激活值设计成了torch.channels_last。这时候用torch.empty_like()创建临时缓冲区就比你用torch.empty()再手动转换布局要省心得多也少踩一堆“shape 对但 layout 不对”的坑。4. 训练场景中两种方法带来的真实影响4.1 未初始化数据如何悄悄污染 loss理论讲再多不如看一个实际案例。假设你在写一个自定义训练循环想给每个 batch 准备一个累积梯度的缓冲区。一个常见写法是这样的accum torch.empty(4, 10) for i, batch in enumerate(dataloader): out model(batch) loss criterion(out, target) accum loss * 0.1这段代码的问题在于accum在第一次累加之前里面是未初始化的内存残留。accum loss * 0.1会先读取accum的旧值再加上新的 loss。这个旧值可能是 0可能是 1e10也可能是nan。一旦撞上nan后续所有梯度更新都会跟着变nan。有人可能会说“我用了torch.empty()但它打印出来是 0 啊是不是没事”这里有个很大的陷阱empty()的内存残留有时恰好是 0因为操作系统可能恰好分配了一块被清零过的内存页或者上一位使用者刚好释放了一块全 0 的数据。但这种“恰好”是完全不可靠的换个运行环境、换个批次大小甚至换个时间运行结果可能就不一样了。正确的做法很简单在做累加、累乘、减少这类操作之前要么用torch.zeros()初始化缓冲区要么在第一次写入前把值覆盖掉。记住一个原则torch.empty()适合“马上会被整体覆盖”的存储场景不适合“边读边写”的累积场景。4.2 GPU 上的分配差异CPU 与 CUDA 的行为不同在 CPU 上torch.empty()和torch.Tensor()的行为都依赖于 glibc 的malloc内存页可能是刚被系统清零的也可能是别人用过的。有趣的是很多操作系统为了安全会倾向于给新进程分配清零的内存页所以你在 CPU 上连续执行torch.empty()经常看到全 0 的输出误以为它和torch.zeros()没区别。但到了 GPU 上就完全不同了。CUDA 的显存分配器通常会维护一个缓存池释放掉的显存块会很快被重新分配给新的张量。这意味着torch.empty()在 GPU 上更容易碰到上一步计算残留的数据。我在一次训练中打印过一个大矩阵里面的值居然是一个已删除中间张量的真实特征值数值范围、分布都跟当前任务完全无关。如果这种张量被误用进 loss训练过程轻则震荡重则直接发散。所以在 GPU 上调试时不要轻信torch.empty()打印出来“看起来正常”的值。如果你需要确定性的零初始化就用torch.zeros()如果你确定后续会全覆盖再用empty()来节约那一次显存写入。4.3 从版本演进看 PyTorch 的推荐方向PyTorch 官方目前对torch.Tensor()的态度是“能用但不推荐在新代码里使用”。原因很简单它的行为不够明确同一个函数在不同参数下承担了完全不同的职责这在工程上是很大的维护成本。新代码里官方推荐的几个创建方式分别是根据数据创建torch.tensor(data)创建未初始化张量torch.empty(...)、torch.empty_like(...)创建全零张量torch.zeros(...)、torch.zeros_like(...)创建全一张量torch.ones(...)、torch.ones_like(...)创建单位矩阵torch.eye(...)创建随机值张量torch.randn(...)、torch.randint(...)我自己在新项目里几乎不用torch.Tensor()做构造。搜索项目代码时如果看到torch.Tensor(出现在非类型注解的位置我一般都会把它改成torch.tensor、torch.empty或torch.zeros中的某一个具体取决于代码意图。这样改动之后代码可读性通常会有明显提升。5. 常见问题与排查技巧实录5.1 一张表说清楚选型逻辑为了方便查阅我把常见场景和推荐写法整理成了一张表目标推荐写法理由从 Python 列表创建torch.tensor([1, 2, 3])行为明确默认 int64 或 float32从张量复制一份独立数据x.clone()或torch.tensor(x)克隆数据不共享内存创建与 x 同形状的未初始化张量torch.empty_like(x)自动继承形状、dtype、layout创建与 x 同形状的全 0 张量torch.zeros_like(x)适合做累加缓冲区创建指定形状但后续会覆盖的内存torch.empty(2, 3)省去初始化写入创建指定形状且需要确定初值 0torch.zeros(2, 3)避免内存残留按 api 文档要求使用默认 float 类型torch.FloatTensor(...)或torch.empty(...)明确指定类型避免歧义创建连续内存布局的 4D 张量torch.empty(b, c, h, w, memory_formattorch.channels_last)适合卷积优化5.2 打印结果看着像随机数是 bug 吗这是遇到最多的问题。torch.Tensor(2, 3)或torch.empty(2, 3)打印出来一串大数字第一反应总是“代码写错了”。我的排查顺序是这样的判断张量是不是刚通过empty或Tensor(size)创建的。如果是那打印出来的异常值大概率是内存残留不是算术错误。看后续有没有对这块内存做“全覆盖式”赋值。比如copy_、fill_、normal_等原地操作。只要有张量后续参与运算就是安全的。如果张量在创建后直接参与loss output - target这类运算而输出打印异常先把它改成torch.zeros()再跑一次。如果结果变正常说明问题就是未初始化残留导致的。如果是在模型内部出现nan也不要急着怀疑反向传播先用同样的输入检查一下前向过程里有没有用到empty创建的中间变量。有一次一个朋友训练 Transformer 时 loss 一直到第三次迭代才出现nan。他查了很久的梯度、学习率和数据最后发现只是注意力模块里一个(batch, seq, dim)的中间张量用了torch.empty()来“临时存一下”结果第一个 token 位置的旧数据把 softmax 前的 logits 污染了。这说明未初始化数据不光影响打印值还可能潜伏好几个步骤才爆雷。5.3 比较 torch.Tensor(x)、torch.tensor(x)、x.clone() 的区别这个对比非常值得展开。三者看起来都能从已有数据产生一个新张量但内存关系完全不同torch.Tensor(x)当 x 是张量时新张量与 x共享底层存储修改一个会影响另一个。这里需要特别强调这种行为只对“张量输入”生效如果输入是列表或数组则会复制数据。torch.tensor(x)当 x 是张量时会复制一份数据二者互不影响。但要注意torch.tensor()总是会触发数据拷贝所以如果 x 已经是一个张量直接用x.clone()通常会更快因为torch.tensor()内部还要做类型推断和拷贝。x.clone()复制一份数据同时保留梯度信息如果要复制一个带梯度的张量且不想保留梯度用x.detach().clone()。有个实际场景很典型你要做数据增强在原张量上随机遮掉一部分元素但又不能影响原始输入。这时候用torch.Tensor(x)就会出大事——你改的是共享内存原数据会一起被改掉。正确做法是用x.clone()得到独立副本再对副本做 mask 操作。5.4 一个容易忽视的点dtype 和 device 的默认值torch.Tensor()创建出来的张量默认永远是torch.float32不管你传入的数据是整数还是浮点数t torch.Tensor([1, 2, 3]) print(t.dtype) # torch.float32 t2 torch.tensor([1, 2, 3]) print(t2.dtype) # torch.int64这点在写数据处理代码时很容易埋雷。比如你从 JSON 里读了一批整数 ID用torch.Tensor(id_list)转成张量想着后面当作索引去查 embedding。结果它变成了浮点张量索引直接报错。而torch.tensor()会保留整数的类型行为更符合直觉。同时torch.Tensor(2, 2)创建出来的对象永远在 CPU 上。如果你想在 GPU 上创建张量必须写成torch.Tensor(2, 2).cuda()或者更优雅地使用torch.empty(2, 2, devicecuda)。在分布式训练或多卡环境下我一般都会在代码的配置信息里定义好device然后所有张量都显式传入devicedevice避免隐式地留在 CPU 上。5.5 给新项目的一条具体建议统一张量创建风格经过长时间的使用我在团队里定了一条不成文的规范任何张量的创建都要能一眼看出它的内容语义。内容来自数据用torch.tensor(data, dtype..., device...)内容全为零或全为一用torch.zeros/torch.ones内容要随机初始化用torch.randn/torch.rand内容后续完全覆盖用torch.empty想复制一份独立数据用.clone()或.detach().clone()尽量避免直接调用torch.Tensor()来做张量创建这样做的好处是当你回头读三个月前写的代码或者别人接手你的代码时不需要去验证torch.Tensor在这里到底是拷贝、共享还是空分配。代码的第一眼信息就足够判断意图了。6. 实操环节在代码里验证所有差异6.1 基础对比脚本我建议你新建一个 Python 文件把下面的代码完整跑一遍。这个脚本会把本文提到的关键行为全部验证一遍import torch # 1. 按尺寸创建两者都是未初始化 a torch.Tensor(2, 3) b torch.empty(2, 3) print(a (Tensor by size):, a) print(b (empty by size):, b) # 2. 从列表创建 c torch.Tensor([1, 2, 3]) d torch.tensor([1, 2, 3]) print(c dtype:, c.dtype) # torch.float32 print(d dtype:, d.dtype) # torch.int64 # 3. 从张量创建torch.Tensor 共享内存torch.tensor 复制 x torch.arange(5) y torch.Tensor(x) z torch.tensor(x) y[0] 100 z[1] 999 print(x after y[0]100:, x) # x[0] 被修改了 print(x after z[1]999:, x) # x[1] 不变 # 4. empty_like 继承形状和 dtype base torch.randn(4, 8, dtypetorch.float32) buf torch.empty_like(base) print(buf shape:, buf.shape, dtype:, buf.dtype) # 5. 在 GPU 上创建 if torch.cuda.is_available(): g torch.empty(2, 2, devicecuda:0) print(GPU tensor device:, g.device)6.2 复现未初始化数据的污染下面这个例子可以在你的机器上直观看到未初始化张量对计算的破坏。多运行几次你可能会看到不同的输出import torch # 用一个确定值的张量占住一块内存 source torch.randn(1000) * 10000 # 反复创建和释放让内存里残留 source 相关的数据 for _ in range(10): tmp torch.empty(1000).copy_(source) del tmp # 再创建一个未初始化张量它可能残留前面 source 的值 noise torch.empty(1000) print(noise[:10])注意这个行为在不同操作系统上表现不一样不是每次都能复现出污染值。但它足以说明empty()的结果不是一个可靠的“0”也不是一个可靠的“随机数”它只是内存的残影。6.3 CPU 上看似为 0GPU 上却出脏数据如果你手头有 GPU建议跑一下这个对比import torch cpu_t torch.empty(4, 4) print(CPU empty:\n, cpu_t) if torch.cuda.is_available(): gpu_t torch.empty(4, 4, devicecuda:0) print(GPU empty:\n, gpu_t)很多时候 CPU 的empty()输出是 0而 GPU 的empty()输出是明显的残留值。这不是 PyTorch 的 bug而是 CPU 和 GPU 内存分配机制不同导致的。我在实际项目里把张量创建从torch.zeros改成torch.empty以提速时就会特别注意运行环境是不是 GPU以及后续会不会立刻覆盖整块显存。7. 核心要点回顾创建张量前先问自己三个问题讲了这么多最后把经验收敛成几个可执行的问题。每次你要创建一个新张量时先在脑子里过一遍这三个问题基本就不会踩坑了。第一个问题这块内存的内容重要吗如果重要比如要参与 loss 计算、梯度累加、或作为权重参数那就必须用torch.zeros、torch.tensor、torch.randn这类有确定初始值的创建方式。如果内容不重要也不参与任何读取只是当作临时复用缓冲区那用torch.empty是合理的选择。第二个问题我创建的是一块新数据还是只是已有数据的一个新视图如果需要新数据用torch.tensor或.clone()如果只是想共享内存、省去拷贝可以用torch.Tensor(existing_tensor)的共享行为但建议写成更明确的tensor.view()或tensor.as_strided()让意图暴露在代码表面。第三个问题dtype 和 device 是否明确在大项目里默认的float32和cpu并不一定是你想要的。torch.Tensor()在这方面限制很大因为它不提供dtype和device参数。torch.empty()、torch.zeros()、torch.tensor()都可以显式指定也更适合工程化使用。我个人在实际开发中的体会是torch.Tensor()这种“看起来全能、实则行为分裂”的 API是历史包袱的典型代表。新写的代码没必要继承这个包袱。把创建张量的职责拆分给torch.tensor、torch.empty、torch.zeros这几个语义单一的函数长期来看能少很多调试时间也让代码库更容易维护。如果你正维护的旧代码里大量使用了torch.Tensor()也不用急着一次性改完可以在每次修改相关模块时顺手替换并补上对应的单元测试这样慢慢就迁干净了。