前言
上文我们通过针对性的阅读pytorch源码的框架结构,同时以__init__.py
文件为线索探索了pytorch中多个主要类型的实现和功能。在上文中我们曾提到,pytorch框架是一个以python为前端,C++为后端的框架。如果去除掉pytorch中的python前端,那我们就可以得到一个C++的AI框架——libtorch
。没有特殊说明时,本文所看源码均是取自libtorch
而非pytorch
,其中libtorch
的源码版本是libtorch-win-shared-with-deps-2.1.0+cpu
。
一个引子
我们来看看如下代码:
torch::Tensor x = torch::tensor({1.0});
是否觉得很眼熟,其实这就是当我们在python中调用torch.tensor()
这个函数时,其在C++后端实际上调用的函数,从这一个例子中我们可以知道,要想真正理解pytorch的底层原理,就需要深入的理解libtorch的源码。
目录结构
和上文我们解析pytorch源码一样,现在我们来看一下libtorch的源码结构,值得注意的是,和pytorch一样,libtorch
也分为cpu版本和gpu版本两个版本,因此其目录结构稍有不同。我们主要看cpu版本的include
目录以及lib
目录,lib
目录下存放的是静态链接文件,而include
下存放的是头文件,我们在使用libtorch
的时候,实际上是引入头文件,最后在编译过程中,由编译器根据头文件去找到路径下的动态链接文件,并在链接期间将其链接进来。
include
目录下则是这个样子,可以很容易地发现,这和pytorch的源码结构是几乎一样的。torch.h解读
torch.h
源码很短,只有如下几行,从中我们可以看到它其实是引入了all.h
这个头文件,另外还引入了一个extension.h
文件。这里这个extension.h
文件其实是引入了python.h
,当然,由于这里讨论的是无python前端的libtorch
版本,因此不需要注意这个。
#pragma once
#include <torch/all.h>
#ifdef TORCH_API_INCLUDE_EXTENSION_H
#include <torch/extension.h>
#endif // defined(TORCH_API_INCLUDE_EXTENSION_H)
在进入all.h
后,可以看到如下代码,很明显,这个头文件就是将所有模块下的头文件引入,现阶段我们要关注的只有linalg.h
这一个文件。
#pragma once
#if !defined(_MSC_VER) && __cplusplus < 201703L
#error C++17 or later compatible compiler is required to use PyTorch.
#endif
#include <torch/autograd.h>
#include <torch/cuda.h>
#include <torch/data.h>
#include <torch/enum.h>
#include <torch/fft.h>
#include <torch/jit.h>
#include <torch/linalg.h>
#include <torch/mps.h>
#include <torch/nested.h>
#include <torch/nn.h>
#include <torch/optim.h>
#include <torch/serialize.h>
#include <torch/sparse.h>
#include <torch/special.h>
#include <torch/types.h>
#include <torch/utils.h>
#include <torch/version.h>
torch::Tensor浅析
进入linalg.h
后我们可以看到如下代码,这个文件提供了相当多的内联函数,主要是一些矩阵运算相关的函数。当然,这个文件最重要的部分其实是将ATen.h
引入了torch.h
中,记得上文我们说过ATen
目录是与Tensor
类实现相关的库,也就是说,在这个文件中我们可以找到torch::Tensor
的声明。
#pragma once
#include <ATen/ATen.h>
namespace torch {
namespace linalg {
#ifndef DOXYGEN_SHOULD_SKIP_THIS
namespace detail {
inline Tensor cholesky(const Tensor& self) {
return torch::linalg_cholesky(self);
}
...
进入到ATen.h
文件后,我们看到如下代码,其中出现了我们在上一节提到过的两个核心概念,Tensor
和Storage
,它们被包含在ATen/Tensor.h
以及c10/core/Storage.h
中。
#pragma once
#if !defined(_MSC_VER) && __cplusplus < 201703L
#error C++17 or later compatible compiler is required to use ATen.
#endif
#include <ATen/Context.h>
#include <ATen/Device.h>
#include <ATen/DeviceGuard.h>
#include <ATen/DimVector.h>
#include <ATen/Dispatch.h>
#include <ATen/Formatting.h>
#include <ATen/Functions.h>
#include <ATen/NamedTensor.h>
#include <ATen/ScalarOps.h>
#include <ATen/Tensor.h>
#include <ATen/TensorGeometry.h>
#include <ATen/TensorIndexing.h>
#include <ATen/TensorOperators.h>
#include <ATen/Version.h>
#include <ATen/core/ATenGeneral.h>
#include <ATen/core/Generator.h>
#include <ATen/core/Reduction.h>
#include <ATen/core/Scalar.h>
#include <ATen/core/UnsafeFromTH.h>
#include <ATen/core/ivalue.h>
#include <ATen/core/jit_type.h>
#include <c10/core/Allocator.h>
#include <c10/core/InferenceMode.h>
#include <c10/core/Layout.h>
#include <c10/core/Storage.h>
#include <c10/core/TensorOptions.h>
#include <c10/util/Exception.h>
// TODO: try to remove this
// There is some back story, see https://github.com/pytorch/pytorch/issues/48684
#include <ATen/NativeFunctions.h>
进入ATen/Tensor.h
中,可以看到该文件其实引入了ATen/core/Tensor.h
,因此我们进入该文件,进入后我们可以看到如下代码:
#pragma once
#include <ATen/core/TensorBody.h>
#include <c10/util/Exception.h>
namespace at {
class TORCH_API OptionalTensorRef {
public:
OptionalTensorRef() = default;
~OptionalTensorRef() {
ref_.unsafeReleaseTensorImpl();
}
OptionalTensorRef(const TensorBase& src)
: ref_(Tensor::unsafe_borrow_t{}, src) {
TORCH_INTERNAL_ASSERT_DEBUG_ONLY(src.defined());
}
OptionalTensorRef(const OptionalTensorRef& rhs)
: ref_(Tensor::unsafe_borrow_t{}, rhs.ref_) {}
OptionalTensorRef& operator=(OptionalTensorRef rhs) {
std::swap(ref_, rhs.ref_);
return *this;
}
bool has_value() const {
return ref_.defined();
}
const Tensor& getTensorRef() const & {
return ref_;
}
const Tensor& operator*() const & {
return ref_;
}
const Tensor* operator->() const & {
return &ref_;
}
operator bool() const {
return ref_.defined();
}
private:
Tensor ref_;
};
// Use to convert a TensorBase (that may be undefined) to an at::Tensor
// without bumping refcount.
class TORCH_API TensorRef {
public:
~TensorRef() {
ref_.unsafeReleaseTensorImpl();
}
TensorRef(const TensorBase& src)
: ref_(Tensor::unsafe_borrow_t{}, src) {}
const Tensor& operator*() const & {
return ref_;
}
private:
Tensor ref_;
};
template <typename T>
auto Tensor::register_hook(T&& hook) const -> Tensor::hook_return_void_t<T> {
// Return the grad argument in case of a hook with void return type to have an
// std::function with Tensor return type
static_assert(std::is_same<decltype(hook(Tensor())), void>::value,
"Expected hook to return void");
return _register_hook([fn=std::forward<T>(hook)](const TensorBase& grad_base) {
TensorRef grad(grad_base);
fn(*grad);
return Tensor();
});
}
template <typename T>
auto Tensor::register_hook(T&& hook) const -> Tensor::hook_return_var_t<T> {
return _register_hook([fn=std::forward<T>(hook)](const TensorBase& grad_base) {
TensorRef grad(grad_base);
Tensor ret = fn(*grad);
return TensorBase(std::move(ret));
});
}
} // namespace at
这段代码其实主要是实现了ATen/core/TensorBody.h
中声明的部分类型,同时,最重要的torch::Tensor
类型也是在ATen/core/TensorBody.h
这个文件中声明的,因此我们进入这个文件。该文件中第92行到1458行为torch::Tensor
的定义。从中我们可以看到,该类继承于TensorBase
类,同时没有子类,也就是说在libtorch中,我们不能使用类似于pytorch中的torch.FloatTensor()
这类函数初始化一个Tensor
对象。这个现象在上文中其实有所体现,因为这些类型是在python中进行派生的。
class TORCH_API Tensor: public TensorBase {
protected:
// Create a Tensor with a +0 reference count. Special care must be
// taken to avoid decrementing this reference count at destruction
// time. Intended to support MaybeOwnedTraits<Tensor>.
explicit Tensor(unsafe_borrow_t, const TensorBase& rhs): TensorBase(unsafe_borrow_t{}, rhs) {}
friend MaybeOwnedTraits<Tensor>;
friend OptionalTensorRef;
friend TensorRef;
public:
Tensor() = default;
// This constructor should not be used by end users and is an implementation
// detail invoked by autogenerated code.
explicit Tensor(
c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl> tensor_impl)
: TensorBase(std::move(tensor_impl)) {}
Tensor(const Tensor &tensor) = default;
Tensor(Tensor &&tensor) = default;
...
torch::Storage浅析
现在我们来看看Storage
类,抛去那些繁琐的查找,我们看到c10/core/Storage.h
头文件的内容,该文件主要是Storage
类的定义。
#pragma once
#include <c10/core/StorageImpl.h>
namespace c10 {
struct C10_API Storage {
public:
struct use_byte_size_t {};
Storage() = default;
Storage(c10::intrusive_ptr<StorageImpl> ptr)
: storage_impl_(std::move(ptr)) {}
// Allocates memory buffer using given allocator and creates a storage with it
Storage(
use_byte_size_t /*use_byte_size*/,
SymInt size_bytes,
Allocator* allocator = nullptr,
bool resizable = false)
: storage_impl_(c10::make_intrusive<StorageImpl>(
StorageImpl::use_byte_size_t(),
std::move(size_bytes),
allocator,
resizable)) {}
// Creates storage with pre-allocated memory buffer. Allocator is given for
// potential future reallocations, however it can be nullptr if the storage
// is non-resizable
Storage(
use_byte_size_t /*use_byte_size*/,
size_t size_bytes,
at::DataPtr data_ptr,
at::Allocator* allocator = nullptr,
bool resizable = false)
: storage_impl_(c10::make_intrusive<StorageImpl>(
StorageImpl::use_byte_size_t(),
size_bytes,
std::move(data_ptr),
allocator,
resizable)) {}
...
在这部分,我们主要粗略地看了torch::Tensor
以及torch::Storage
的定义和部分实现,在继续翻看源码之前,我们在中间插入一些理论上的部分,这个部分主要和pytorch的设计理念有关。
Tensor原理介绍
我们先来学习下Tensor的实现原理,即官方在实现Tensor的过程中遵循了怎样的思想。Tensor 是PyTorch的核心数据结构,它是包含若干个标量(标量可以是各种数据类型如浮点型、整形等)的n-维的数据结构。我们可以认为tensor包含了数据和元数据(metadata),元数据用来描述tensor的大小、其包含内部数据的类型、存储的位置(CPU内存或是CUDA显存),而数据则是tensor真正的物理存储的数据。简单来说,我们对一个Tensor
对象进行操作的时候,比如切分,resize
等操作,实际上并不会改变这个Tensor
对象在物理上的存储位置,而是通过操作metadata来改变这个Tensor
对象的“逻辑表示”。
元数据metadata中有我们已经熟知的一些属性,如
device
,sizes
,dtype
,同样的也存在layout
,strides
这些我们以前并未了解过的属性。我们先来解释
strides
步长的概念,首先我们在上方已经提到,操作Tensor
对象实际上是操作metadata的过程,例如执行以下代码:
import torch
a = torch.tensor([[1, 2],[3, 4]])
print(a[1, 0])
我们可以很容易的知道这段代码打印的结果是3,因为这符合我们的编程直觉和习惯。那么,当我们对a
这个变量进行索引的时候,其底层究竟是怎么做的呢。根据我们在上文所讲述的,一个Tensor对象分为数据和元数据,数据其实是连续分配在某设备device
上的,而元数据中的strides
可以实现索引与数据物理位置的一一映射,具体我们看下方这张图:
Tensor是一个数学概念。当用计算机表示数学概念的时候,通常我们需要定义一种物理存储方式。最常见的表示方式是将Tensor中的每个元素按照次序连续的在内存中铺开,将每一行写到相应内存位置里。如上图所示,假设tensor包含的是32位的整数,因此每个整数占据一块物理内存,每个整数的地址都和上下相邻整数相差4个字节。为了记住tensor的实际维度,我们需要将tensor的维度大小记录在额外的元数据中。假设我想要访问位于tensor [1, 0]位置处的元素,如何将这个逻辑地址转化到物理内存的地址上呢?步长就是用来解决这样的问题:当我们根据下标索引查找tensor中的任意元素时,将某维度的下标索引和对应的步长相乘,然后将所有维度乘积相加就可以了。在上图中我将第一维(行)标为红色,第二维(列)标为蓝色,因此你能够在计算中方便的观察下标和步长的对应关系。求和返回了一个0维的标量2,而内存中地址偏移量为2的位置正好储存了元素3。
到目前为止,细心的读者应该已经可以意识到,为什么一个Storage
对象可以对应多个Tensor
对象了。很显然,对于上面所举的例子,其中物理位置就是由Storage
对象进行管理的,而逻辑表示则是由Tensor
对象进行管理,通过不同的strides,sizes,我们可以很轻松地实现将一个Storage
对象映射到多个Tensor
对象中去。
Tensor源码解读
TensorBase类
在开始讲这个类之前,我们先来看一下core\TensorBase.h
引入的头文件,可以看到当中有我们之前提到的一些熟悉的面孔,像Layout.h
,Storage.h
等。
#include <c10/core/Device.h>
#include <c10/core/Layout.h>
#include <c10/core/MemoryFormat.h>
#include <c10/core/ScalarType.h>
#include <c10/core/ScalarTypeToTypeMeta.h>
#include <c10/core/Storage.h>
#include <c10/core/SymIntArrayRef.h>
#include <c10/core/TensorImpl.h>
#include <c10/core/TensorOptions.h>
#include <c10/core/UndefinedTensorImpl.h>
#include <c10/core/WrapDimMinimal.h>
#include <c10/util/Exception.h>
#include <c10/util/ExclusivelyOwned.h>
#include <c10/util/ExclusivelyOwnedTensorTraits.h>
#include <c10/util/MaybeOwned.h>
#include <c10/util/Optional.h>
#include <c10/util/intrusive_ptr.h>
#include <ATen/core/NamedTensor.h>
#include <ATen/core/QuantizerBase.h>
#include <ATen/core/TensorAccessor.h>
#include <ATen/StorageUtils.h>
我们在ATen\core\TensorBase.h
中可以看到TensorBase
类的声明和部分实现,这是所有Tensor类的基类,本文不去细究该类每一个成员函数的作用,而是从宏观的角度介绍这个类。
class TORCH_API TensorBase {
public:
struct unsafe_borrow_t { explicit unsafe_borrow_t() = default; };
protected:
// Create a Tensor with a +0 reference count. Special care must be
// taken to avoid decrementing this reference count at destruction
// time. Intended to support MaybeOwnedTraits<Tensor>.
explicit TensorBase(unsafe_borrow_t, const TensorBase& rhs)
: impl_(c10::intrusive_ptr<at::TensorImpl, UndefinedTensorImpl>::reclaim(rhs.impl_.get())) {}
friend MaybeOwnedTraits<TensorBase>;
...
首先,对于部分C++了解不多的读者可能不清楚在class
和类名TensorBase
之间的TORCH_API
是什么。其实TORCH_API
是一个宏,我们知道宏是在程序的预编译时期起作用的,自然这里的TORCH_API
也一定是由于某种需要在预编译时期进行处理的需求而出现的。我们再来看到开发者在contributing.md
中说到的一段话:
Symbols are NOT exported by default on Windows; instead, you have to explicitly mark a symbol as exported/imported in a header file with __declspec(dllexport) / __declspec(dllimport). We have codified this pattern into a set of macros which follow the convention *_API, e.g., TORCH_API inside Caffe2, Aten and Torch. (Every separate shared library needs a unique macro name, because symbol visibility is on a per shared library basis. See c10/macros/Macros.h for more details.)
其实这里也就说明了为什么要使用TORCH_API
的原因了,因为pytorch是以C、C++为后端,python为前端的框架。想要在python中使用C++,就不免需要用到动态链接库,即将C++的代码导出为.dll
或.so
格式,而在windows上这些函数不能直接导出,必须在头文件中使用__declspec(dllexport)
/__declsspec(dllimport)
显式地将符号标记为导出/导入,而标记方式就是在类前加上这些宏。因此官方采用了一套*_API
的宏用以编码,TORCH_API
就是其中一种。
去查找TORCH_API
这个宏,不难发现其实该宏的定义来自于C10_IMPORT
这个宏。
#define TORCH_API C10_IMPORT
而C10_IMPORT
这个宏又定义于__declspec(dllimport)
,这也印证了前文所述。
#define C10_IMPORT __declspec(dllimport)
除了TORCH_API
以外,这里还有个常用的宏函数TORCH_CHECK
用于对数据合法性进行检查。
intrusive_ptr_target类
TensorBase.h
文件的剩下部分都是对TensorBase
类的成员函数进行定义,几乎每一个成员函数都用到了一个成员变量impl_
,同时这些用到该成员变量的成员函数基本都可以认为是作用在这个成员变量上实现的,找到该文件的第890行,这里给出了该变量的定义:
protected:
void enforce_invariants();
c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl> impl_;
在这里,对这个成员变量而言,我们需要注意两个部分,一是它的数据类型,是个典型的模板类,二是它的泛型声明为了TensorImpl
和UndefinedTensorImpl
两个类型。
我们首先来看TensorImpl
是什么,实际上在TensorBody.h
中有这么一段注释,这段注释位于75到91行,它的大意可以理解为,Tensor
本质上是一个采用引用计数的对象,多个Tensor可以同时指向同一个TensorImpl
。也就是说TensorBase
这个类本质上是通过引用计数的方式指向TensorImpl
对象来实现底层操作的,即TensorBase
可以理解为对TensorImpl
的进一步封装。当然,这样的设计思想在各种工程项目中也随处可见。
// Tensor is a "generic" object holding a pointer to the underlying TensorImpl object, which
// has an embedded reference count. In this way, Tensor is similar to boost::intrusive_ptr.
//
// For example:
//
// void func(Tensor a) {
// Tensor b = a;
// ...
// }
//
// In this example, when we say Tensor b = a, we are creating a new object that points to the
// same underlying TensorImpl, and bumps its reference count. When b goes out of scope, the
// destructor decrements the reference count by calling release() on the TensorImpl it points to.
// The existing constructors, operator overloads, etc. take care to implement the correct semantics.
//
// Note that Tensor can also be NULL, i.e. it is not associated with any underlying TensorImpl, and
// special care must be taken to handle this.
另一方面,它的类型是c10::intrusive_ptr
,而这是pytorch框架最基础,最核心的数据结构代码,我们先来看一下官方是怎么描述的:
/**
* intrusive_ptr<T> is an alternative to shared_ptr<T> that has better
* performance because it does the refcounting intrusively
* (i.e. in a member of the object itself).
* Your class T needs to inherit from intrusive_ptr_target to allow it to be
* used in an intrusive_ptr<T>. Your class's constructor should not allow
*`this` to escape to other threads or create an intrusive_ptr from `this`.
*/
我们去寻找这个类型的定义,会发现在c10/util/intrusive_ptr.h
这个文件下,第54到90行有如下代码,从这段代码可以知道c10::intrusive_ptr
是intrusive_ptr_target
的友元类,除此之外,还有个值得注意的友元类weak_intrusive_ptr
。实际上,PyTorch中使用intrusive_ptr
来管理Tensor
和Storage
的引用计数,其中引用分为强引用和弱引用(弱引用为了解决循环引用问题),对应的类名 intrusive_ptr
和weak_intrusive_ptr
。
class C10_API intrusive_ptr_target {
// Note [Weak references for intrusive refcounting]
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Here's the scheme:
//
// - refcount == number of strong references to the object
// weakcount == number of weak references to the object,
// plus one more if refcount > 0
// An invariant: refcount > 0 => weakcount > 0
//
// - c10::StorageImpl stays live as long as there are any strong
// or weak pointers to it (weakcount > 0, since strong
// references count as a +1 to weakcount)
//
// - finalizers are called and data_ptr is deallocated when refcount == 0
//
// - Once refcount == 0, it can never again be > 0 (the transition
// from > 0 to == 0 is monotonic)
//
// - When you access c10::StorageImpl via a weak pointer, you must
// atomically increment the use count, if it is greater than 0.
// If it is not, you must report that the storage is dead.
//
mutable std::atomic<size_t> refcount_;
mutable std::atomic<size_t> weakcount_;
template <typename T, typename NullType>
friend class intrusive_ptr;
friend inline void raw::intrusive_ptr::incref(intrusive_ptr_target* self);
template <typename T, typename NullType>
friend class weak_intrusive_ptr;
friend inline void raw::weak_intrusive_ptr::incref(
intrusive_ptr_target* self);
template <typename T>
friend struct ExclusivelyOwnedTensorTraits;
当然,不仅如此,除了intrusive_ptr
和weak_intrusive_ptr
是intrusive_ptr_target
的友元类之外,还可以知道的是,TensorImpl
和UndefinedTensorImpl
均是intrusive_ptr_target
的子类,从这里我们也可以看出intrusive_ptr_target
是pytorch得以实现的极为底层的核心数据结构。
TensorImpl类
现在我们来看一下TensorImpl
类是如何组织的,这部分定义位于c10/core/TensorImpl.h
的497到3060行,在这段代码里,我们可以看到一些上文提到过的概念,比如TensorImpl
在初始化的时候其实传入了一个Storage
对象,同时TensorImpl
也出现了上文提到过的sizes
。由于TensorBase
本质上是对TesorImpl
的引用,那么Tensor
也就是对TensorImpl
的引用,所有当我们创建torch::Tensor
类型时,实际上是执行了这段代码。
struct C10_API TensorImpl : public c10::intrusive_ptr_target {
TensorImpl() = delete;
~TensorImpl() override;
// Note [Enum ImplType]
// This enum is temporary. In the followup refactor we should
// think about how to specialize TensorImpl creation for view
// tensors. Currently we only special case its key_set_ but
// there's also potential to share version_counter_ directly
// without creating first and then override in as_view.
enum ImplType { VIEW };
/**
* Construct a 1-dim 0-size tensor backed by the given storage.
*/
TensorImpl(
Storage&& storage,
DispatchKeySet,
const caffe2::TypeMeta data_type);
// See Note [Enum ImplType]
TensorImpl(
ImplType,
Storage&& storage,
DispatchKeySet,
const caffe2::TypeMeta data_type);
...
TensorImpl(const TensorImpl&) = delete;
TensorImpl& operator=(const TensorImpl&) = delete;
TensorImpl(TensorImpl&&) = delete;
TensorImpl& operator=(TensorImpl&&) = delete;
...
public:
/**
* Return a reference to the sizes of this tensor. This reference remains
* valid as long as the tensor is live and not resized.
*/
IntArrayRef sizes() const {
if (C10_UNLIKELY(matches_policy(SizesStridesPolicy::CustomSizes))) {
return sizes_custom();
}
return sizes_and_strides_.sizes_arrayref();
}
...
StorageImpl类
StorageImpl
类位于c10/core/StorageImpl.h
中,它也是intrusive_ptr_target
的子类,它的构造函数里有不少pytorch中的核心概念,如Allocator,resizable等,这些概念这篇文章不做讨论,将在后续的系列谈及。
struct C10_API StorageImpl : public c10::intrusive_ptr_target {
public:
struct use_byte_size_t {};
StorageImpl(
use_byte_size_t /*use_byte_size*/,
SymInt size_bytes,
at::DataPtr data_ptr,
at::Allocator* allocator,
bool resizable)
: data_ptr_(std::move(data_ptr)),
size_bytes_(std::move(size_bytes)),
size_bytes_is_heap_allocated_(size_bytes_.is_heap_allocated()),
resizable_(resizable),
received_cuda_(false),
allocator_(allocator) {
if (resizable) {
TORCH_INTERNAL_ASSERT(
allocator_, "For resizable storage, allocator must be provided");
}
}
StorageImpl(
use_byte_size_t /*use_byte_size*/,
const SymInt& size_bytes,
at::Allocator* allocator,
bool resizable)
: StorageImpl(
use_byte_size_t(),
size_bytes,
size_bytes.is_heap_allocated()
? allocator->allocate(0)
: allocator->allocate(size_bytes.as_int_unchecked()),
allocator,
resizable) {}
StorageImpl& operator=(StorageImpl&& other) = delete;
StorageImpl& operator=(const StorageImpl&) = delete;
StorageImpl() = delete;
StorageImpl(StorageImpl&& other) = delete;
StorageImpl(const StorageImpl&) = delete;
~StorageImpl() override = default;
...
总结,本文主要讲述了Tensor
的源码实现机理,以及Storage
和Tensor
的关系,下一篇文章将会讲述pytorch是如何实现C++与python绑定的。