简单看看tensor/stoage相关的实现,为后面的autograd相关的做一下准备
StorageImpl
StorageImpl = 一块连续内存 buffer 的所有者。它只关心三件事:内存在哪(data_ptr_)、有多大(size_bytes_)、谁来分配/释放(allocator_)
- stride,shape,dtype相关的则是由TensorImpl来负责
注释中:
// NB: storage is supposed to uniquely own a data pointer; e.g.,
// two non-null data pointers alias if and only if they are from
// the same storage.
即data_ptr_相同,那么storage一定是相同的。storage唯一拥有底层的data ptr
在python层,会根据storage是否相同来判断是否是同一份数据。
Version counting也是发生在storage层。所以这个storage层更像是一个功能更多的data buffer
实现上,继承自intrusive_ptr_target,是一个share ptr,用来把share ptr的控制块inline进去
关键的成员变量:
```C++
DataPtr data_ptr_; // 内存本体(指针 + deleter + device)
SymInt size_bytes_; // 字节数(可能是符号)
bool size_bytes_is_heap_allocated_; // size 是不是符号
bool resizable_;
Allocator* allocator_; // 怎么分配/释放
impl::PyObjectSlot pyobj_slot_; // 关联的 Python 对象
<pre><code class="line-numbers">- 存储数据的buffer就是data ptr,以及对应的size\_bytes\_了。因为这一层没有dtype等,所以都是按照bytes来计算的空间
- size\_bytes\_支持SymInt,里面是一个union,可以是一个i64的size,也可以是一个指针,指向一个SymNode。这个等compile的时候再详细研究
- resizable和allocator有一个联动,如果是resizable的话,需要allocator存在,才能通过allocator进行resize
- 当然也需要保证resizable\_为true才行,比如share memory这种,resizable就是false,因为已经可能share给其他人了
- pyobj\_slot\_对应每一个storage impl一个,python层访问Storage对象的时候应该访问的就是这里。
<br>
COW,校验相关
```C++
bool has_mutable_data_ptr_check_; // 慢路径汇总开关
bool throw_on_mutable_data_ptr_;
bool throw_on_immutable_data_ptr_;
bool warn_deprecated_on_mutable_data_ptr_;
MaterializeFn materialize_fn_; // COW 钩子
代码中有几个地方的校验,并不是常态开启的,手动设置后才会开启,应该是对应一些错误的链路。
COW
然后来看下这里COW是怎么做的
cow主要是用来做lazy clone,只有当修改新的tensor的时候才执行拷贝。
api在COW.h的lazy_clone_storage上,主要是通过这个例子来看一下stoage impl中的materialize_fn_是怎么使用的
设计的核心思路就是把原本的data ptr换成一个带引用计数的data ptr。这样就会出现两个storage共享同一个data ptr的情况。此时删除一个storage,另一个还会持有对应的数据。
对应的deleter则也是通过COW这里wrap了一层,保证引用计数为0的时候才能释放
Materialize fn是materialize_cow,会直接操作StorageImpl,修改其中的data ptr。这里分两种情况:
- 如果refcnt为1,表示当前storage是最后一个引用者,直接把data ptr移动走即可,不需要拷贝
-
否则的话会通过allocator->clone(),拷贝+复制内存,放到新的data ptr上。
对应到StorageImpl中,就是在调用mutable_ptr的时候,涉及到对数据的修改,就需要触发这里的materialize,复制一份新的出来。就完成了COW
使用的话,主要是torch._lazy_clone(t),好像看起来用的不多。应该是因为平常没有这种复杂的用法,基本上一个tensor变化完了老的tensor就释放掉了。
TensorImpl
tensor impl的几个作用:
- 负责管如何看待一块数据buffer,处理shape/stride/dtype等信息
-
autograd相关,autograd相关的元信息存在这一层
-
Dispatch key set,用于做dispatch
-
还有一些自定义逻辑的处理,custom layout等,这块暂时先不看
先看解释数据相关的,核心的成员:
| 成员 | 含义 |
|---|---|
Storage storage_ |
指向底层数据 |
SizesAndStrides sizes_and_strides_ |
sizes 和 strides 打包在一起 |\ |
| 注意这里的单位是element,而非bytes | |
int64_t storage_offset_ |
在 storage 里的起始偏移(元素数,非字节) |
int64_t numel_ |
元素总数 = sizes 连乘,冗余缓存,避免每次重算 |
caffe2::TypeMeta data_type_ |
dtype |\ |
| 是一个uint16 | |
std::optional<Device> device_opt_ |
device |\ |
| 包含一个device type |\ | |
| 和一个device index |
SizesAndStrides是把size和stride打包到一起,对应每一个维度的size和stride。有一个inline的优化类似inline vector一样。就是维度小于5的时候,size和stride会inline到对象里面,大于5的时候会单独分配一个buffer出来保存这个数组。
autograd相关的字段:
```C++
// This pointer points to an AutogradMeta struct that stores autograd-specific
// fields (such as grad_ / grad_fn_ / grad_accumulator_)...
std::unique_ptr
c10::VariableVersion version_counter_;
- `autograd_meta_`:存 `grad_` / `grad_fn_` 等。优化点:不需要梯度的张量这里是 `nullptr`,省内存。
- `version_counter_`它和 storage 共享版本号,in-place 会 bump 它。
- 用来给autograd使用,追踪是否发生了inplace的修改,避免影响autograd的语意
- 在view/reshape等view类操作的时候,base tensor和新的tensor会共享version\_counter\_,里面是一个share\_ptr<atomic int>。
<br>
看一下这里version counter的bump链路:
```Python
x.add_(1)
│ 经 Dispatcher 按 DispatchKeySet 分发,先命中 ADInplaceOrView key
▼
ADInplaceOrView::add_ ← 代码生成的 kernel(见下)
│ ① 设 guard,redispatch 到真正改数据的 kernel(CPU/CUDA)
│ ② increment_version(self)
▼
torch::autograd::increment_version(self) # VariableTypeUtils.h:158
▼
torch::autograd::impl::bump_version(self) # variable.cpp:363
▼
self.unsafeGetTensorImpl()->bump_version() # TensorImpl.h:2224
▼
version_counter_.bump() # TensorImpl.h:2225
▼
++version_counter_->version_ (atomic<uint32_t>) # TensorImpl.h:408-417
</code></pre>
在torch/csrc/autograd/generated/ADInplaceOrViewTypeEverything.cpp中有生成好的kernel代码,编译就可以看到:
```C++
at::Tensor & add__Tensor(c10::DispatchKeySet ks, at::Tensor & self, const at::Tensor & other, const at::Scalar & alpha) {
{
at::AutoDispatchBelowADInplaceOrView guard;
at::_ops::add__Tensor::redispatch(ks & c10::after_ADInplaceOrView_keyset, self, other, alpha);
}
increment_version(self);
return self;
}
```
可以看到这里是先redispatch,把InplaceOrView这个dispatch key去掉了,进入到下一层的计算中。
计算结束后做increment_version,就是到上面的调用链路图,最后是bump version
还有一个需要注意的点是单位:
- size/stride的单位是element
-
storage相关的是bytes
所以as_strided/view(dtype)的接口是分开的,因为dtype的变化影响的是stride/size如何parse到对应的storage offset。
文章评论