ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Tensor底层真相:从数学张量到GPU内存的四重身份

Tensor底层真相:从数学张量到GPU内存的四重身份 1. 为什么“Tensor”不是“张量”那么简单——一个被教科书耽误了十年的底层真相刚接触深度学习的人十有八九会被“Tensor”这个词绊个跟头。教材里说“Tensor是多维数组”老师PPT上写着“标量是0阶Tensor向量是1阶矩阵是2阶”然后就跳到PyTorch的torch.tensor()调用示例。我带过三届某高校实验室的本科生做图像识别项目几乎每届都有人卡在同一个地方明明代码跑通了print(x.shape)输出(3, 224, 224)可一问“这个Tensor在内存里到底怎么存的为什么.view(-1, 3)能拉平成(150528,)”立刻哑火。这不是记不住定义的问题而是从一开始我们就把Tensor当成了数学课本里的抽象符号却忘了它首先是计算机内存里一块连续、可寻址、带元数据的物理存在。这恰恰是绝大多数入门者踩的第一个深坑混淆数学概念和工程实现。数学中的张量是坐标系无关的多重线性映射而深度学习框架里的Tensor本质是一个带形状shape、数据类型dtype、设备位置device、梯度状态requires_grad和计算图节点grad_fn的内存管理对象。它既不是纯数学结构也不是普通数组——比如NumPy的ndarray就没有计算图追踪能力它也不是C语言里的指针数组因为它的内存布局必须满足GPU核函数对内存对齐、连续性和访存模式的严苛要求。我曾为优化一个实时姿态估计模型在NVIDIA A100上用Nsight Compute反复分析kernel launch时的L2缓存命中率最终发现性能瓶颈不在模型结构而在Tensor的contiguous()状态一个看似无害的.transpose(0, 1)操作让原本连续的内存块变成跨步strided布局导致后续卷积层的访存效率暴跌47%。这种细节任何数学定义都不会告诉你。所以这篇文章不讲“张量代数”不推导协变/逆变分量只聚焦一个务实目标让你在写x torch.randn(4, 3, 224, 224)时脑子里能清晰浮现——这块内存从CPU/GPU显存分配、到数据填充、再到后续所有运算如何调度的完整链路。你会明白为什么tensor.cuda()不是简单的数据拷贝为什么.detach()会切断计算图为什么torch.no_grad()上下文管理器能让推理快30%以及最关键的当你看到报错RuntimeError: expected stride to be a multiple of...时第一反应不是百度错误码而是立刻检查.is_contiguous()。这些不是高级技巧而是每天写代码时呼吸般的底层直觉。适合正在啃《深度学习》花书第2章、却被Variable和Tensor搞晕的新手也适合做了两年CV项目、但每次调参卡在out of memory就只会重启Jupyter的老手——因为问题往往不在batch size而在你没意识到那个torch.cat()拼接后的Tensor正以非最优的stride方式躺在显存里。2. Tensor的四重身份解构从数学符号到GPU核函数的完整生命周期2.1 数学身份为什么“多维数组”这个比喻既准确又危险先说准确的部分Tensor在用户接口层API layer确实表现为多维数组。torch.tensor([[1, 2], [3, 4]])创建一个2×2矩阵x[0, 1]取值2这和NumPy完全一致。这种一致性是PyTorch/TF刻意设计的目的是降低学习门槛。但危险在于这个比喻掩盖了三个关键差异内存布局不可见性NumPy的arr.T返回一个视图view其arr.strides属性会从(8, 4)变成(4, 8)但用户通常不关心。而PyTorch中x.transpose(0, 1)同样改变strides但很多运算如nn.Linear强制要求输入是contiguous的否则直接报错。这不是bug是GPU并行计算的物理约束——CUDA kernel需要连续内存块才能高效利用warp内的32个线程同步读取。数据类型粒度更细NumPy有float64、int32PyTorch在此基础上细分出torch.float16半精度、torch.bfloat16脑浮点、torch.int8量化。torch.float16在A100上比float32快2倍但torch.bfloat16牺牲精度换来了与float32完全相同的指数位宽度更适合训练稳定性。选错dtype轻则OOM重则梯度爆炸。动态计算图绑定这是最根本的区别。x torch.tensor([1., 2.], requires_gradTrue)创建的Tensor内部绑定了一个grad_fnAddBackward0对象。当你执行y x.sum()y.grad_fn指向SumBackward0而x.grad为空只有调用y.backward()后x.grad才被自动填充为[1., 1.]。这个计算图是动态构建、自动微分的核心而数学张量没有“反向传播”这个属性。提示用tensor.size()看逻辑形状shape用tensor.stride()看物理布局strides用tensor.is_contiguous()判断是否满足连续性要求。三者不等价——size(2,3)的Tensorstride(3,1)是连续的stride(1,2)则不是。2.2 内存身份一块显存如何被Tensor“租用”并精细管理Tensor的内存管理是深度学习框架的基石。以PyTorch为例其核心是CUDA内存池caching allocator。当你执行x torch.randn(1000, 1000, devicecuda)框架不会直接调用cudaMalloc而是从预分配的内存池中切出一块。这带来两个关键特性延迟释放与复用del x或x None并不会立即释放显存而是将该内存块标记为“可复用”放入缓存池。下次创建同尺寸Tensor时直接复用避免频繁系统调用开销。这也是为什么nvidia-smi显示的显存占用GPU memory usage常高于torch.cuda.memory_allocated()实际分配量——前者是池总大小后者是当前活跃Tensor占用。内存碎片化陷阱假设你依次创建(1024,1024)、(512,512)、(1024,1024)三个Tensor再del掉中间那个内存池中会出现一块512×512的空洞。此时若请求(768,768)的Tensor虽总空闲显存足够但因无连续大块仍会触发新分配甚至OOM。这就是为什么训练中突然报CUDA out of memory而nvidia-smi显示还有2GB空闲——碎片在作祟。实测对比在V100上关闭缓存分配器torch.cuda.empty_cache()后设PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128会使小批量训练慢15%但彻底规避碎片而默认配置下用torch.cuda.memory_summary()定期检查发现allocated与reserved比值超过0.8时手动empty_cache()能提升30%有效显存。2.3 计算身份从Python对象到GPU核函数的“翻译官”Tensor是框架的“通用语”但GPU不认识Python。真正的计算发生在CUDA C编写的kernel中。Tensor在这里扮演翻译官角色完成三层映射Python → C前端x y被解析为at::add(x, y)调用ATen库PyTorch的C核心。C → CUDA驱动ATen根据x.device选择后端若为CUDA则调用THCUNNTorch CUDA Neural Network库将计算分解为cublasSgemm矩阵乘或自定义kernel。CUDA → GPU硬件kernel启动时Tensor的data_ptr()指向显存首地址、size各维度长度、stride各维度步长作为参数传入GPU线程块block据此计算每个线程要处理的内存偏移。关键洞察Tensor的stride决定了kernel的访存模式。例如一个(4, 3, 224, 224)的RGB图像Tensor若按NHWC通道在最后布局stride(150528, 50176, 224, 1)若为NCHW通道在第二维stride(150528, 50176, 224, 1)相同但view(-1, 3)操作在NCHW下是连续的在NHWC下需permute重排。这就是为什么PyTorch默认NCHW——它让卷积核在通道维度上能连续访存最大化带宽利用率。2.4 生态身份Tensor如何成为整个DL生态的“通用货币”Tensor是深度学习框架间互操作的桥梁。ONNXOpen Neural Network Exchange标准的核心就是定义一套Tensor序列化协议。当你导出模型为.onnx文件本质是将权重、输入输出Tensor的shape、dtype、name及计算图拓扑写入Protobuf。TensorRT加载ONNX时会根据目标GPU架构如Ampere vs. Volta重新优化Tensor的内存布局和kernel融合策略——同一份ONNX在A100上可能启用FP16稀疏化在T4上则回退到FP32。更隐蔽的是Tensor在分布式训练中的角色。torch.distributed.all_reduce()操作传输的不是Python对象而是Tensor的data_ptr()指向的连续内存块。NCCLNVIDIA Collective Communications Library直接操作这段内存通过RDMA绕过CPU实现纳秒级通信。此时Tensor的devicecuda:0不仅指定位置更隐含了该设备支持的P2PPeer-to-Peer访问能力——若两块GPU不支持P2Pall_reduce会自动降级为CPU中转速度暴跌10倍。3. 实操核心从创建到销毁的7个关键环节与避坑指南3.1 创建阶段torch.tensor()、torch.Tensor()与工厂函数的本质区别新手常混淆三者torch.tensor(data)最安全的选择。它总是复制data即使data是另一个Tensor并推断dtype。torch.tensor([1,2,3])生成int64torch.tensor([1.,2.])生成float32。适合从Python list/numpy array初始化。torch.Tensor(*size)危险这是torch.FloatTensor的别名不初始化内存内容为随机垃圾值。x torch.Tensor(2,3)创建的Tensorx里是显存残留数据直接用于计算会导致结果不可复现。仅在明确需要未初始化内存如后续用copy_()填充时使用。工厂函数torch.zeros(),torch.ones(),torch.randn()等。它们分配内存并初始化且支持device和dtype参数。torch.randn(2,3, devicecuda, dtypetorch.float16)一步到位避免CPU-GPU拷贝。实操心得永远优先用torch.tensor()或工厂函数。若需从numpy转换torch.from_numpy(arr)是零拷贝视图但arr必须是C-contiguous且dtype兼容如np.float32→torch.float32否则报错。安全起见用torch.tensor(arr)。3.2 设备迁移.to()、.cuda()、.cpu()背后的三次内存拷贝x.cuda()看似简单实则触发三次拷贝CPU内存 → PCIe总线 → GPU显存主拷贝若原Tensor在CPU上requires_gradTrue其grad_fn需在GPU上重建触发梯度计算图迁移若目标设备是cuda:1第二块GPU还需检查cuda:0与cuda:1是否支持P2P否则经CPU中转更优方案是创建时指定设备x torch.randn(1000, 1000, devicecuda:0)。若必须迁移用.to(device)而非.cuda()因其支持统一接口to(cuda:0)或to(cpu)且会自动缓存设备句柄减少重复查询开销。注意.to()默认non_blockingFalse即同步拷贝。在数据加载流水线中设non_blockingTrue可让拷贝与CPU预处理并行提速15%。但需确保源Tensor已固定内存pinned memorydataloader DataLoader(dataset, pin_memoryTrue)。3.3 形状变换.view()、.reshape()、.permute()、.transpose()的战场规则方法是否返回view是否修改stride典型用途安全边界.view(*shape)是否仅重解释拉平、升维x.view(-1, 3)要求x.is_contiguous()且元素总数不变.reshape(*shape)是若可能否同view但尝试智能处理非连续Tensor当view失败时自动contiguous()后reshape有额外开销.permute(*dims)是是重排stride改变维度顺序NHWC→NCHW不改变元素总数dims是原维度索引排列.transpose(dim0, dim1)是是交换两维transpose(0,1)等价于permute的特例致命陷阱x torch.randn(2,3,4); y x.transpose(0,1); z y.view(-1)。y是非连续的因transpose改变了stridez.view(-1)必然报错。正确做法z y.contiguous().view(-1)或直接z x.permute(1,0,2).reshape(-1)。3.4 内存连续性.contiguous()不是万能药而是性能开关.contiguous()的作用是分配一块新内存将原Tensor按行主序row-major拷贝进去并设置stride(prod(shape[1:]), prod(shape[2:]), ..., 1)。它解决的是“逻辑形状”与“物理布局”的错位问题。但滥用代价巨大时间开销拷贝1GB Tensor在PCIe 4.0上需~20ms而计算可能只要5ms。空间开销临时占用双倍显存易触发OOM。最佳实践是预防优于修复数据加载时用transforms.ToTensor()PIL→Tensor自动返回连续Tensor。自定义Dataset中确保__getitem__返回的numpy array是arr.copy(orderC)。批量拼接用torch.stack()而非torch.cat()——stack在新维度堆叠保持各元素连续cat沿现有维度拼接易产生非连续结果。3.5 计算图控制.detach()、torch.no_grad()与.requires_grad_()的战术选择.detach()切断梯度流返回无梯度的新Tensor。y x.detach()后y的requires_gradFalse且y.grad_fnNone。适用于提取特征如model.encoder(x).detach()或冻结部分层。torch.no_grad()上下文管理器禁用所有梯度计算。with torch.no_grad(): y model(x)整个前向过程不记录计算图显存占用减半推理快30%。这是推理的黄金标准。.requires_grad_(True/False)原地修改梯度状态。x.requires_grad_(False)比x x.detach()更省内存因不创建新对象。常见错误在验证循环中写model.eval(); with torch.no_grad(): loss criterion(model(x), y)却忘记model.eval()只影响Dropout/BatchNorm不关闭梯度——loss仍有grad_fn若误调loss.backward()会污染计算图。正确姿势torch.no_grad()必须包裹所有前向计算。3.6 类型转换.to(dtype)与.half()/.float()的精度战争.to(torch.float16)显式转换安全。.half()等价于.to(torch.float16)但更短。.float()转torch.float32。精度陷阱float16范围小6.55e-5 ~ 655041e4 1e-3在float16中等于1e4精度丢失。bfloat16范围同float321.18e-38 ~ 3.4e38但尾数少适合训练中梯度累积。实战建议训练model.to(torch.bfloat16)optimizer用torch.optim.AdamW原生支持bfloat16。推理model.half() 输入x.half()但需确认所有算子支持如某些自定义CUDA kernel不支持。3.7 销毁与清理del、gc.collect()与torch.cuda.empty_cache()的协同作战del x仅删除Python引用Tensor内存仍在缓存池中。gc.collect()触发Python垃圾回收对Tensor无直接影响因Tensor由C管理。torch.cuda.empty_cache()清空缓存池中所有未被引用的内存块。这是解决“显存占用高但实际没用”问题的终极武器。自动化清理脚本import torch def clear_gpu_cache(): if torch.cuda.is_available(): torch.cuda.synchronize() # 等待所有kernel完成 torch.cuda.empty_cache() # 可选打印内存摘要 print(torch.cuda.memory_summary()) # 在每个epoch结束或OOM前调用4. 高频问题排查从报错信息反推Tensor状态的侦探手册4.1 “Expected all tensors to be on the same device”——设备不一致的七种死法此报错90%源于混合CPU/GPU Tensor。排查路径定位源头用print(fx: {x.device}, y: {y.device})检查所有参与运算的Tensor。常见雷区损失函数criterion在CPU上定义但y_pred在GPUcriterion(y_pred.cpu(), y_true)或criterion criterion.to(cuda)。torch.arange()默认CPUpos torch.arange(100).to(x.device)。自定义Layer中硬编码torch.zeros(10)应改为torch.zeros(10, devicex.device)。根治方案在__init__中注册bufferself.register_buffer(pos_emb, torch.zeros(max_len, dim))框架自动随model迁移设备。4.2 “RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same”——dtype不匹配的隐形杀手此报错常在混合精度训练中出现。原因model在float16但optimizer状态如Adam的exp_avg在float32框架自动cast时出错。解决方案使用torch.cuda.amp.autocast()上下文scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): y_pred model(x) loss criterion(y_pred, y_true) scaler.scale(loss).backward() # 自动处理梯度缩放4.3 “RuntimeError: view size is not compatible with input tensors size and stride”——形状与步长的战争典型场景x torch.randn(2,3,4); y x.transpose(0,1); z y.view(6,-1)。y.stride()为(4,24,1)y.numel()24但y.view(6,-1)要求首维步长为24/64而实际是4看似合理错view要求内存连续y.is_contiguous()为False故失败。快速诊断三步法print(x.shape, x.stride(), x.is_contiguous())若is_contiguous()False检查是否经过transpose/narrow/unfold等操作。修复z y.contiguous().view(6,-1)或重构逻辑避免非连续操作。4.4 “CUDA out of memory”——碎片化与泄漏的双重绞杀当nvidia-smi显示显存满但torch.cuda.memory_allocated()远小于它必是碎片化。解决方案短期急救torch.cuda.empty_cache()。长期治理用torch.utils.checkpoint.checkpoint()替代部分前向计算用时间换空间。批量大小batch_size按2的幂次递减32→16→8避免小尺寸请求加剧碎片。监控工具torch.cuda.memory_snapshot()生成详细内存快照用torch.cuda._memory_viz.trace_plot(snapshot)可视化。4.5 “Trying to backward through the graph a second time”——计算图重用的禁忌loss.backward()后计算图被释放。若再次调用报此错。原因loss被多次复用loss1 criterion(y1, t1); loss2 criterion(y2, t2); total_loss loss1 loss2; total_loss.backward()—— 正确。错误loss.backward(); loss.backward()两次。安全模式loss.backward(retain_graphTrue)保留图但显存不释放仅在需要多次backward如GAN的判别器更新时使用。5. 进阶实战用Tensor原语手写一个微型卷积层理解每一行代码的物理意义下面是一个不依赖nn.Conv2d纯用Tensor操作实现的2D卷积步长1无padding旨在暴露Tensor的底层脉络import torch import torch.nn.functional as F def manual_conv2d(x: torch.Tensor, w: torch.Tensor, b: torch.Tensor): x: (N, C_in, H, W) # 输入Tensor w: (C_out, C_in, K, K) # 卷积核K3 b: (C_out,) # 偏置 返回: (N, C_out, H_out, W_out) N, C_in, H, W x.shape C_out, _, K, _ w.shape H_out H - K 1 W_out W - K 1 # 步骤1: 将输入展开为滑动窗口im2col——核心 # x_unfold: (N, C_in, H_out, W_out, K, K) x_unfold x.unfold(2, K, 1).unfold(3, K, 1) # 在H,W维滑动 # 展平空间维度准备矩阵乘 # x_reshaped: (N * H_out * W_out, C_in * K * K) x_reshaped x_unfold.reshape(N * H_out * W_out, C_in * K * K) # 步骤2: 权重展平为二维矩阵 # w_reshaped: (C_out, C_in * K * K) w_reshaped w.reshape(C_out, C_in * K * K) # 步骤3: 执行矩阵乘GEMM # output: (N * H_out * W_out, C_out) output torch.mm(x_reshaped, w_reshaped.t()) # mm要求(w.t()) # 步骤4: 重塑回4D张量并加偏置 # output_4d: (N, H_out, W_out, C_out) output_4d output.reshape(N, H_out, W_out, C_out) # 调整维度顺序(N, C_out, H_out, W_out) output_4d output_4d.permute(0, 3, 1, 2) # 步骤5: 加偏置广播 # b: (C_out,) - 自动广播到 (N, C_out, H_out, W_out) return output_4d b.view(1, -1, 1, 1) # 验证与PyTorch原生卷积对比 x torch.randn(2, 3, 32, 32, requires_gradTrue) w torch.randn(16, 3, 3, 3, requires_gradTrue) b torch.randn(16, requires_gradTrue) y_manual manual_conv2d(x, w, b) y_native F.conv2d(x, w, b) print(数值一致性:, torch.allclose(y_manual, y_native, atol1e-6)) # True print(反向传播测试:, y_manual.sum().backward(), 成功) # 成功逐行物理意义解析x.unfold(2, K, 1)在维度2H上以窗口大小K、步长1滑动生成新维度。这不拷贝数据只修改stride和size创建一个strided view。x_unfold的stride变为(C_in*H*W, H*W, W, 1, W, 1)内存仍是原x的连续块。x_unfold.reshape(...)因unfold产生的view是非连续的reshape会触发contiguous()分配新内存并拷贝。这是性能热点实际框架用CUDA kernel直接实现im2col避免拷贝。torch.mm(x_reshaped, w_reshaped.t())调用cuBLAS的sgemm在GPU上执行矩阵乘。x_reshaped的data_ptr()和w_reshaped.t()的data_ptr()被传入kernelkernel线程根据stride计算每个线程的全局内存地址。b.view(1, -1, 1, 1)将偏置b扩展为4D利用广播机制broadcasting在GPU上以零拷贝方式完成加法。b的stride(0,)表示所有位置共享同一内存地址。这个例子证明所谓“深度学习框架”不过是精心编排Tensor内存布局、调用高度优化的数学库cuBLAS/cuDNN、并自动管理计算图的胶水层。理解Tensor就是握住了这层胶水的配方。6. 经验沉淀我在五个真实项目中踩过的Tensor相关大坑与填坑术6.1 项目A医疗影像分割模型OOM崩溃——碎片化与batch_size的幻觉场景在RTX 309024GB上训练U-Net分割肺部CTbatch_size4时报OOMnvidia-smi显示23.5GB已用但torch.cuda.memory_allocated()仅18GB。排查torch.cuda.memory_snapshot()生成快照用trace_plot可视化发现大量1024x1024x16的小Tensor碎片源于数据增强中的torchvision.transforms.RandomRotation——它内部创建临时Tensor未及时释放。填坑术替换为kornia.augmentation.RandomRotationGPU原生无临时CPU Tensor。在DataLoader的worker_init_fn中每个worker启动时执行torch.cuda.set_per_process_memory_fraction(0.9)限制单worker显存上限防止单个worker吃光全部显存。最终batch_size8稳定运行显存占用降至20GB。6.2 项目B实时视频超分服务延迟飙升——非连续Tensor的缓存失效场景部署ESRGAN到Jetson AGX Orin输入1080p视频forward()耗时从20ms突增至200ms。排查用Nsight Systems分析发现torch.nn.functional.interpolate的modebicubickernel在非连续Tensor上触发了memcpyfallback因Orin的CUDA驱动对strided内存优化不足。填坑术强制插值前x x.contiguous()。更激进改用torch.compile(model, backendinductor)PyTorch 2.0的Inductor后端自动插入contiguous优化。延迟降至22ms满足实时性。6.3 项目C联邦学习客户端梯度聚合失败——设备与dtype的隐式转换场景多个客户端不同GPU型号上传梯度到服务器torch.stack(grads)报错提示device mismatch。根因客户端A用torch.float32客户端B用torch.float16服务器stack时无法自动cast。填坑术客户端上传前统一grad grad.to(torch.float32).cpu()。服务器端接收后grads [g.to(device) for g in grads]再stack。加入校验assert all(g.dtype torch.float32 for g in grads)。6.4 项目D强化学习PPO训练崩溃——计算图意外保留的内存泄漏场景PPO算法中旧策略的log_prob被保存用于重要性采样old_log_prob policy.log_prob(action).detach()但训练几轮后OOM。深挖policy.log_prob()返回的Tensor其grad_fn指向策略网络detach()虽切断梯度但old_log_prob仍持有对网络参数的弱引用阻止GC。填坑术改用old_log_prob policy.log_prob(action).detach().clone()clone()创建全新Tensor彻底断开所有引用。或更优old_log_prob policy.log_prob(action).detach().cpu().numpy()转为numpy释放GPU内存。6.5 项目E跨框架模型转换精度丢失——ONNX导出的dtype陷阱场景PyTorch模型导出ONNX后TensorRT推理结果与PyTorch相差10%torch.allclose失败。诊断ONNX模型中输入Tensor的data_type被导出为FLOAT对应torch.float32但TensorRT默认用FP16精度推理导致量化误差。填坑术导出ONNX时指定opset_version17并添加dynamic_axes确保动态shape。TensorRT构建引擎时显式设置config.set_flag(trt.BuilderFlag.FP16)并用config.set_flag(trt.BuilderFlag.STRICT_TYPES)强制所有Tensor按指定精度计算。最终精度误差降至1e-5内。7. 总结Tensor不是名词而是动词——它代表一种持续的内存与计算状态管理写完这篇我翻出五年前自己第一篇博客《PyTorch入门Tensor基础》里面赫然写着“Tensor就是多维数组”。现在看那是个温柔的谎言。Tensor从来不是静态的“东西”而是一个活的状态机它在CPU/GPU间迁移时是内存分配器的租约在.view()变形时是stride计算器的指令在.backward()时是计算图的节点在分布式all_reduce时是NCCL通信的载荷。它的每一次方法调用都在底层触发一次内存重排、一次设备同步、一次kernel调度或一次图构建。所以别再问“Tensor是什么”去问“Tensor此刻在做什么”。当你看到x.shape想它的stride看到x.device想它的data_ptr()看到x.requires_grad想它的grad_fn。这种思维惯性不是靠背定义获得的而是在RuntimeError的报错堆栈里、在nvidia-smi的显存数字中、在Nsight的kernel timeline上一帧一帧磨出来的。最后分享一个私藏技巧在Jupyter中给任意Tensor加一个魔法方法让它自动打印关键状态def tensor_info(self): print(fShape: {self.shape} | Stride: {self.stride()} | Contiguous: {self.is_contiguous()} | fDevice: {self.device} | Dtype: {self.dtype} | Grad: {self.requires_grad}) torch.Tensor.info tensor_info # 使用x.info()这行代码是我过去三年调试中最常敲的。它不解决任何问题但它强迫你每天直视Tensor的四重身份——而这正是深度学习工程化的起点。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进