前言

在深度学习模型训练中,每次迭代过程中都涉及到Tensor的创建和销毁,伴随着的是内存的频繁 mallocfree操作,可能对模型训练带来不必要的 overhead。

在主流的深度学习框架中,会借助 chunk 机制的内存池管理技术来避免这一点。通过实事先统一申请不同 chunk size 的内存,并记录到内存池中。创建一个Tensor时,若内存池中存在满足需求的可用内存,则直接分配。销毁一个Tensor时,并不马上free掉还给系统,而是标记为可用状态,放在内存池供下个Tensor使用。

通过内存池管理技术,可以有效减少频繁的mallocfree操作,避免不必要的overhead。

技术实现

chunk

每个chunk代表一段连续的存储空间。不同的chunk按照地址升序组成双向链表。每个chunk只有两种状态:空闲、已占用。不存在部分使用的中间态。

在Paddle中,内存池统一通过 BuddyAllocator类来管理,下面逐一剖析相关实现。成员变量包括:

private:
/*
* 默认的内存分配器,支持CPUAllocator、GPUAllocator、CUDAPinnedAllocator。
*/
std::unique_ptr<SystemAllocator> system_allocator_; // 用于表示一个内存段的信息
using IndexSizeAddress = std::tuple<size_t, size_t, void*>;
// 借助有序的 set 存放可用的内存段
using PoolSet = std::set<IndexSizeAddress>; PoolSet pool_; // 内存池,存放可用的不同 chunk size的内存信息
PoolSet chunks_; // 内存池。存放从系统重新申请的内存块

BuddyAllocator的成员变量可以看出,不同BuddyAllocator对象可以管理不同类型的内存池,比如 CPU内存池、GPU内存池、CUDAPinned内存池。

构造函数显式需要一个SystemAllocator来初始化:

public:
BuddyAllocator(std::unqiue_ptr<SystemAllocator> system_allocator, size_t min_chunk_size, size_t max_chunk_size);

内存申请

BuddyAllocator如何避免内存频繁的mallocfree操作呢?

申请内存时:

void* BuddyAllocator::Alloc(size_t unaligned_size){
// step 1: 做内存对齐,保证申请的内存大小都是 min_chunk_size的整数倍
size_t size = align(unaligned_size+sizeof(MemoryBlock::Desc), min_chunk_size_); // 加锁
std::lock_guard<std::mutex> lock(mutex_); // step 2: 如果申请内存超过 max_chunk_size_, 则交由system_allocator完成
if(size > max_chunk_size_){
return SystemAlloc(size);
} // step 3: 否则,去内存池查找是否有满足大小的可用内存块
auto it = FindExistChunk(size); // step 4: 若找不到,则向系统申请新内存块,并记录到内存池中
if(it == pool_.end()){
it = RefillPool(size);
if(it == pool_.end()){
return nullptr;
}
}else{
VLOG(10)<<;
} // step 5: 更新内存池 size 相关信息
total_used_ += size;
total_free_ -= size; // step 6: 若申请的size小于内存块实际大小,则把多余的部分切分掉,新建一个内存块放到内存池中
return reinterpret_cast<MemoryBlock*>(SplitToAlloc(it, size))->Data();
}

内存释放

此处并非真正的将内存归还给系统,而是将内存块从占用状态标记为可用状态,并放到内存池中开放出去。

void BuddyAllocator::Free(void* p){
// step 1: 将指针转换为内存块指针
auto block = static_cast<MemoryBlock*>(p)->MetaData(); std::lock_guard<std::mutex> lock(mutex_);
// step 2: 获取内存块的详细元信息,释放内存需要
auto* desc = cache_.LoadDesc(block);
if(desc->get_type() == MemoryBlock::HUGE_CHUNK){
// 在前面申请大内存时,也是交由system_allocator完成的,解铃还须系铃人
system_allocator_->Free(block, desc->get_totoal_size(), desc->get_index());
// 删除内存块对应的元信息
cache_.Invalidate(block);
return;
} // step 3: 若待释放内存块大小在[min_chunk_size_, max_chunk_size_]之间
block->MarkAsFree(&cache_); // 修改元信息,标记为 可用 状态 // step 4: 更新总内存信息
total_used_ -= desc->get_total_size();
total_free += desc->get_total_size(); // step 5: 看是否可以将此内存块与左右空闲的内存块合并,避免内存碎片
MemoryBlock* right_buddy = block->GetRightBuddy(&cache_);
if(right_buddy){
auto rb_desc = cache_.LoadDesc(right_buddy);
if(rb_desc->get_type() == MemoryBlock::FREE_CHUNK){
pool_.erase(IndexSizedAddress(rb_desc->get_index(), rb_desc->get_total_size(), right_buddy));
block->Merge(&cache_, right_buddy);
}
} MemoryBlock* left_buddy = block->GetLeftBuddy(&cache_);
// .... (省略对前序内存块的合并操作) // step 6: 将合并后的内存块放入到可用内存池中
pool_.insert(IndexSizeAddress(desc->get_index(), desc->get_total_size(), block));
}

内存归还

此阶段才是真正的将内存归还给操作系统,此过程分为两个步骤:

  • 把后来的、通过system_allocator_申请的内存 free掉(调用Release函数)
  • 析构BuddyAllocator对象时,对内存池剩余的内存 free掉(调用析构函数

我们先看第一阶段 Release逻辑:

uint64_t BuddyAllocator::Release(){
// 先加锁
std::lock_guard<std::mutex> lock(mutex_);
int num = 0; // 标记后来新增申请的内存块
uint64_t bytes = 0; // 统计总共可释放的内存
bool del_flag = false;
// step 1: 有序遍历可用内存池中的每个内存块
for(auto iter = pool_.begin(); iter != pool_.end()){
auto remain_size = std::get<1>(*iter);
auto remain_ptr = std::get<2>(*iter); for(auto& chunk : chunks_){
auto init_size = std::get<1>(chunk);
auto init_ptr = std::get<2>(chunk);
// step 2: 若在之前的chunks_记录中找到地址一样,空间一样的chunk
if(init_size = remain_size && init_ptr == remain_ptr){
++num;
bytes += init_size;
total_free_ -= init_size;
auto block = static_cast<MemoryBlock*>(init_ptr);
// step 3: 则归还内存给系统,标记为此内存块为可回收状态
system_allocator_->Free(init_ptr, init_size, std::get<0>(chunk));
cache_.Invalidate(block);
del_flag = true;
break;
}
}
// step 4: 对于标记为可回收状态的内存块,从内存池中移除
if(del_flag){
iter = pool_.erase(iter);
}else{
iter++;
}
}
return bytes;
}

Release支持被显式调用,以归还未用到的内存给操作系统。

BuddyAllocator对象在模型训练结束后,会被析构掉。析构时需要保证之前申请的内存必须正确的归还给操作系统,否则会导致内存泄露。

BuddyAllocator::~BuddyAllocator(){
while(!pool.empty()){
// step 1: 遍历内存池中所有的内存块
auto block = static_cast<MemoryBlock*>(std::get<2>(pool_.begin()));
auto desc = cache_.LoadDesc(block);
// step 2: Free掉,归还给系统
system_allocator_->Free(block, desc->get_total_size(), desc->get_index());
// step 3: 删除元信息
cache_.Invalidata(block);
pool_.erase(pool_.begin());
}
}

参考资料

  1. Paddle 框架源码

Paddle源码之内存管理技术的更多相关文章

  1. MongoDB源码概述——内存管理和存储引擎

    原文地址:http://creator.cnblogs.com/ 数据存储: 之前在介绍Journal的时候有说到为什么MongoDB会先把数据放入内存,而不是直接持久化到数据库存储文件,这与Mong ...

  2. Memcached源码分析——内存管理

    注:这篇内容极其混乱 推荐学习这篇博客.博客的地址:http://kenby.iteye.com/blog/1423989 基本元素item item是Memcached中记录存储的基本单元,用户向m ...

  3. Linux 2.6 源码学习-内存管理-buddy算法

    核心数据结构 linux 2.6 的内存管理支持NUMA(Non Uniform Memory Access Achitecture),即非一致内存访问体系,在该体系中存在多个CPU,并且拥有分离的存 ...

  4. redis源码笔记-内存管理zmalloc.c

    redis的内存分配主要就是对malloc和free进行了一层简单的封装.具体的实现在zmalloc.h和zmalloc.c中.本文将对redis的内存管理相关几个比较重要的函数做逐一的介绍 参考: ...

  5. 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 百篇博客分析OpenHarmony源码 | v16.02

    百篇博客系列篇.本篇为: v16.xx 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪 ...

  6. linux内存源码分析 - 内存回收(整体流程)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文 ...

  7. TOMCAT8源码分析——SESSION管理分析(上)

    前言 对于广大java开发者而已,对于J2EE规范中的Session应该并不陌生,我们可以使用Session管理用户的会话信息,最常见的就是拿Session用来存放用户登录.身份.权限及状态等信息.对 ...

  8. Tomcat源码分析——Session管理分析(上)

    前言 对于广大java开发者而已,对于J2EE规范中的Session应该并不陌生,我们可以使用Session管理用户的会话信息,最常见的就是拿Session用来存放用户登录.身份.权限及状态等信息.对 ...

  9. (转)linux内存源码分析 - 内存回收(整体流程)

    http://www.cnblogs.com/tolimit/p/5435068.html------------linux内存源码分析 - 内存回收(整体流程) 概述 当linux系统内存压力就大时 ...

随机推荐

  1. 使用css控制table的cellspacing和cellpadding属性

    HTML默认的表格样式之间有间隙,每次为了解决这些问题,总要在table标签里添加cellspacing和cellpadding,你是否也很厌倦这样的写法, 那么有没有对应的CSS属性能达到相同的效果 ...

  2. 解决 Vmware 服务拒绝访问的问题

    背景 在服务页面想将 VMware NAT Service 设置为自动开启的,但是保存的时候显示拒绝访问,如下图 解决方案 想到在本机的火绒启动项管理里面将 VMware NAT Service 设置 ...

  3. 对pipe downstream的思考&分析

       回到ngx_http_upstream_send_response,如果是buffering,就会进入后面的处理过程,准备一个ngx_event_pipe_t结构的数据,这个结构可以通过upst ...

  4. nginx&http 第三章 惊群

    惊群:概念就不解释了. 直接说正题:惊群问题一般出现在那些web服务器上,Linux系统有个经典的accept惊群问题,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队 ...

  5. windows下命令行设置静态IP

    windows 10 预览版出现无法设置静态IP的bug,只能通过命令行进行设置,开启powershell,然后执行下列的命令即可 下面的"以太网 3" 为你设置的网卡的网卡名称, ...

  6. go编译成exe后,打开出现闪退问题

    今天博主编译了一个go小脚本,编译完成后用自己电脑试了一下没有问题 然而,当换了一台电脑后,一样是win7系统64位,出现了闪退 于是乎博主疯狂百度 看到网上说   入口文件 的顶部改成  packa ...

  7. 计算机&编程语言发展史

    计算机&编程语言发展史 编辑于2020-11-18 计算机的基本组成 计算机的发展经历了哪几代? 第一代 电子管计算机 第二代 晶体管计算机 第三代 集成电路计算机 第四代 大规模和超大规模集 ...

  8. java开发三年,Java中接口的使用你得知道,不然你凭什么涨薪

    接口概述: 接口是Java语言中的一种引用类型,是方法的"集合",所以接口的内部主要就是定义方法,包含常量,抽象方法(JDK 7及以前),额外增加默认方法和静态方法(JDK 8), ...

  9. BT下载器Folx标签功能怎么实现自动的资源分类

    很多经典的电影作品,比如魔戒三部曲.蜘蛛侠系列.漫威动画系列等,在一个系列中都会包含多个作品.如果使用Folx bt种子下载器自带的电影标签的话,会将这些系列电影都归为"电影"标签 ...

  10. C++基础知识篇:C++ 运算符

    运算符是一种告诉编译器执行特定的数学或逻辑操作的符号.C++ 内置了丰富的运算符,并提供了以下类型的运算符: 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 杂项运算符 本章将逐一介绍算术运 ...