前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)

[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)

作者头像
落寞的鱼丶
发布2022-02-22 20:31:56
7630
发布2022-02-22 20:31:56
举报
文章被收录于专栏:afjhahfhahajk

目录

0x00 摘要

在本系列中,我们介绍了 HugeCTR,这是一个面向行业的推荐系统训练框架,针对具有模型并行嵌入和数据并行密集网络的大规模 CTR 模型进行了优化。

本文主要介绍HugeCTR所依赖的输入数据和一些基础数据结构。其中借鉴了HugeCTR源码阅读 这篇大作,特此感谢。因为 HugeCTR 实际上是一个具体而微的深度学习系统,所以它也实现了众多基础功能,值得想研究深度学习框架的朋友仔细研读。

本系列其他文章如下:

[源码解析] NVIDIA HugeCTR,GPU 版本参数服务器 --(1)

[源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2)

0x01 回顾

我们首先回归一下前文内容,流水线逻辑关系如下:

训练流程如下:

基于前文知识,我们接下来看看如何处理数据。

0x02 数据集

HugeCTR 目前支持三种数据集格式,即NormRawParquet,具体格式参见如下:

Fig. 1: (a) Norm (b) Raw (c) Parquet Dataset Formats

2.1 Norm

为了最大化数据加载性能并最小化存储,Norm 数据集格式由一组二进制数据文件和一个 ASCII 格式的文件列表组成。模型文件应指定训练和测试(评估)集的文件名,样本中的元素(键)最大数目和标签维度,具体如图 1(a)所示。

2.1.1 数据文件

一个数据文件是一个读取线程的最小读取粒度,因此每个文件列表中至少需要10个文件才能达到最佳性能。数据文件由header和实际表格(tabular )数据组成。

Header定义:

代码语言:javascript
复制
typedef  struct DataSetHeader_ {
   long  long error_check;       //0: 没有错误检查;1:check_num 
  long  long number_of_records; //此数据文件中的样本数
  long  long label_dim;          //标签的维度
  long  long density_dim;        //密集特征的维度
  long  long slot_num;           //每个嵌入的 slot_num 
  long  long reserved[ 3 ];      //保留以备将来使用
数据集头;

数据定义(每个样本):

代码语言:javascript
复制
typedef struct Data_ {
  int length;                   //此示例中的字节数(可选:仅在 check_sum 模式下)
  float label[label_dim];
  float dense[dense_dim];
  Slot slots[slot_num];
  char checkbits;               //此样本的校验位(可选:仅在checksum模式下)
} Data;

typedef struct Slot_ {
  int nnz;
  unsigned int*  keys; //可在配置文件的 `solver` 对象中使用 `"input_key_type"` 更改为 `long long`
} Slot;

数据字段(field)通常有很多样本。每个样本以格式化为整数的标签开始,然后是nnz(非零数)和使用 long long(或无符号整数)格式的输入key,如图 1(a)所示。

categorical 的输入key分布到插槽(slot)中,不允许重叠。例如:slot[0] = {0,10,32,45}, slot[1] = {1,2,5,67}。如果有任何重叠,它将导致未定义的行为。例如,给定slot[0] = {0,10,32,45}, slot[1] = {1,10,5,67},查找10键的表将产生不同的结果,结果根据插槽分配给 GPU 的方式。

2.1.2 文件列表

文件列表的第一行应该是数据集中数据文件的数量,然后是这些文件的路径,具体如下所示:

代码语言:javascript
复制
$ cat simple_sparse_embedding_file_list.txt
10
./simple_sparse_embedding/simple_sparse_embedding0.data
./simple_sparse_embedding/simple_sparse_embedding1.data
./simple_sparse_embedding/simple_sparse_embedding2.data
./simple_sparse_embedding/simple_sparse_embedding3.data
./simple_sparse_embedding/simple_sparse_embedding4.data
./simple_sparse_embedding/simple_sparse_embedding5.data
./simple_sparse_embedding/simple_sparse_embedding6.data
./simple_sparse_embedding/simple_sparse_embedding7.data
./simple_sparse_embedding/simple_sparse_embedding8.data
./simple_sparse_embedding/simple_sparse_embedding9.data

使用例子如下:

代码语言:javascript
复制
reader = hugectr.DataReaderParams(data_reader_type = hugectr.DataReaderType_t.Norm,
                                  source = ["./wdl_norm/file_list.txt"],
                                  eval_source = "./wdl_norm/file_list_test.txt",
                                  check_type = hugectr.Check_t.Sum)

2.2 Raw

Raw 数据集格式与 Norm 数据集格式的不同之处在于训练数据出现在一个二进制文件中,并且使用 int32。图 1 (b) 显示了原始数据集样本的结构。

注意:此格式仅接受独热数据。

Raw数据集格式只能与嵌入类型 LocalizedSlotSparseEmbeddingOneHot 一起使用。

例子:

代码语言:javascript
复制
reader = hugectr.DataReaderParams(data_reader_type = hugectr.DataReaderType_t.Raw,
                                  source = ["./wdl_raw/train_data.bin"],
                                  eval_source = "./wdl_raw/validation_data.bin",
                                  check_type = hugectr.Check_t.Sum)

2.3 Parquet

Parquet 是一种面向列的、开源的数据格式。它可用于 Apache Hadoop 生态系统中的任何项目。为了减小文件大小,它支持压缩和编码。图 1 (c) 显示了一个示例 Parquet 数据集。有关其他信息,请参阅parquet 文档

请注意以下事项:

  • Parquet 数据加载器当前不支持嵌套列类型。
  • 不允许列中有任何缺失值。
  • 与 Norm 数据集格式一样,标签和密集特征列应使用浮点格式。
  • Slot 特征列应使用 Int64 格式。
  • Parquet 文件中的数据列可以按任何顺序排列。
  • 要从每个 parquet 文件中的所有行和每个标签、密集(数字)和槽(分类)特征的列索引映射中获取所需信息,需要一个单独的_metadata.json文件。

例子 _metadata.json:

代码语言:javascript
复制
{
"file_stats": [{"file_name": "file1.parquet", "num_rows": 6528076}, {"file_name": "file2.parquet", "num_rows": 6528076}],
"cats": [{"col_name": "C11", "index": 24}, {"col_name": "C24", "index": 37}, {"col_name": "C17", "index": 30}, {"col_name": "C7", "index": 20}, {"col_name": "C6", "index": 19}],
"conts": [{"col_name": "I5", "index": 5}, {"col_name": "I13", "index": 13}, {"col_name": "I2", "index": 2}, {"col_name": "I10", "index": 10}],
"labels": [{"col_name": "label", "index": 0}]
}

使用如下:

代码语言:javascript
复制
reader = hugectr.DataReaderParams(data_reader_type = hugectr.DataReaderType_t.Parquet,
                                  source = ["./criteo_data/train/_file_list.txt"],
                                  eval_source = "./criteo_data/val/_file_list.txt",
                                  check_type = hugectr.Check_t.Non,
                                  slot_size_array = [278899, 355877, 203750, 18573, 14082, 7020, 18966, 4, 6382, 1246, 49, 185920, 71354, 67346, 11, 2166, 7340, 60, 4, 934, 15, 204208, 141572, 199066, 60940, 9115, 72, 34])

我们提供了通过一个选项slot_size_array,可以为每个插槽添加偏移量。slot_size_array是一个长度等于槽数的数组。为了避免添加offset后出现key重复,我们需要保证第i个slot的key范围在0到slot_size_array[i]之间。我们将以这种方式进行偏移:对于第 i 个槽键,我们将其添加偏移量 slot_size_array[0] + slot_size_array[1] + ... + slot_size_array[i - 1]。在上面提到的配置片段中,对于第 0 个插槽,将添加偏移量 0。对于第一个插槽,将添加偏移量 278899。对于第三个插槽,将添加偏移量 634776。

0x03 CSR 格式

嵌入层是基于CSR格式基础之上搭建的,所以我们首先看看CSR格式。

3.1 什么是CSR

稀疏矩阵指的是矩阵中的元素大部分是0的矩阵,实际上现实问题中大多数的大规模矩阵都是稀疏矩阵,因此就出现了很多专门针对稀疏矩阵的高效存储格式,Compressed Sparse Row(CSR)就是其中之一。

这是最简单的一种格式,每一个元素需要用一个三元组来表示,分别是(行号,列号,数值),对应上图右边的一列。这种方式简单,但是记录单信息多(行列),每个三元组自己可以定位,因此空间不是最优。

CSR需要三类数据来表达:数值,列号,行偏移。它不是用三元组来表示一个元素,而是一个整体编码方式。

  • 数值:一个元素。
  • 列号 :元素的列号,
  • 行偏移:某一行的第一个元素在values里面的起始偏移位置。

上图中,第一行元素1是0偏移,第二行元素2是2偏移,第三行元素5是4偏移,第4行元素6是7偏移。最后会在行偏移之后加上矩阵总的元素个数,本例子中是9。

3.2 HugeCTR 之中的CSR

我们从中找出一个例子看看。因为只是用来存储slot里的sparse key,所以没有列号,因为一个slot里的sparse key可以直接顺序存储。

代码语言:javascript
复制
* For example data:
*   4,5,1,2
*   3,5,1
*   3,2
* Will be convert to the form of:
* row offset: 0,4,7,9
* value: 4,5,1,2,3,5,1,3,2

我们再从源码之中找一些信息 samples/ncf/preprocess-20m.py。

代码语言:javascript
复制
def write_hugeCTR_data(huge_ctr_data, filename='huge_ctr_data.dat'):
    print("Writing %d samples"%huge_ctr_data.shape[0])
    with open(filename, 'wb') as f:
        #write header
        f.write(ll(0)) # 0: no error check; 1: check_num
        f.write(ll(huge_ctr_data.shape[0])) # the number of samples in this data file
        f.write(ll(1)) # dimension of label
        f.write(ll(1)) # dimension of dense feature
        f.write(ll(2)) # long long slot_num
        for _ in range(3): f.write(ll(0)) # reserved for future use

        for i in tqdm.tqdm(range(huge_ctr_data.shape[0])):
            f.write(c_float(huge_ctr_data[i,2])) # float label[label_dim];
            f.write(c_float(0)) # dummy dense feature
            f.write(c_int(1)) # slot 1 nnz: user ID
            f.write(c_uint(huge_ctr_data[i,0]))
            f.write(c_int(1)) # slot 2 nnz: item ID
            f.write(c_uint(huge_ctr_data[i,1]))

3.3 操作类

3.3.1 定义

这里只给出成员变量,具体可以和上面csr格式进行印证。

代码语言:javascript
复制
class CSR {
 private:
  const size_t num_rows_;       /**< num rows. */
  const size_t max_value_size_; /**< number of element of value the CSR matrix will have for
                                num_rows rows. */

  Tensor2<T> row_offset_tensor_;
  Tensor2<T> value_tensor_; /**< a unified buffer for row offset and value. */
  T* row_offset_ptr_;       /**< just offset on the buffer, note that the length of it is
                             * slot*batchsize+1.
                             */
  T* value_ptr_;            /**< pointer of value buffer. */

  size_t size_of_row_offset_; /**< num of rows in this CSR buffer */
  size_t size_of_value_;      /**< num of values in this CSR buffer */

  size_t check_point_row_;   /**< check point of size_of_row_offset_. */
  size_t check_point_value_; /**< check point of size_of_value__. */
}
3.3.2 构造函数

构造函数之中,会在GPU之上进行分配内存。

代码语言:javascript
复制
/**
 * Ctor
 * @param num_rows num of rows is expected
 * @param max_value_size max size of value buffer.
 */
CSR(size_t num_rows, size_t max_value_size)
    : num_rows_(num_rows),
      max_value_size_(max_value_size),
      size_of_row_offset_(0),
      size_of_value_(0) {
  std::shared_ptr<GeneralBuffer2<CudaHostAllocator>> buff =
      GeneralBuffer2<CudaHostAllocator>::create();
  buff->reserve({num_rows + 1}, &row_offset_tensor_);
  buff->reserve({max_value_size}, &value_tensor_);
  buff->allocate();

  row_offset_ptr_ = row_offset_tensor_.get_ptr();
  value_ptr_ = value_tensor_.get_ptr();
}
3.3.3 生成新行

new_row 之中会生成新行,并且把目前value总数设置到row_offset之中。

代码语言:javascript
复制
/**
 * Insert a new row to CSR
 * Whenever you want to add a new row, you need to call this.
 * When you have pushed back all the values, you need to call this method
 * again.
 */
inline void new_row() {  // call before push_back values in this line
  if (size_of_row_offset_ > num_rows_) CK_THROW_(Error_t::OutOfBound, "CSR out of bound");
  row_offset_ptr_[size_of_row_offset_] = static_cast<T>(size_of_value_);
  size_of_row_offset_++;
}
3.3.4 插入数据

这里会插入数据,并且增加value总数。

代码语言:javascript
复制
/**
 * Push back a value to this object.
 * @param value the value to be pushed back.
 */
inline void push_back(const T& value) {
  if (size_of_value_ >= max_value_size_)
    CK_THROW_(Error_t::OutOfBound, "CSR out of bound " + std::to_string(max_value_size_) +
                                       "offset" + std::to_string(size_of_value_));
  value_ptr_[size_of_value_] = value;
  size_of_value_++;
}

0x04 基础数据结构

因为 HugeCTR 实际上是一个具体而微的深度学习系统,所以它也实现了众多基础功能,为了更好的进行分析,我们需要首先介绍一些基础数据结构。以下只给出各个类的成员变量和必要函数。

4.1 张量

首先就是最基础的张量概念。

4.1.1 TensorBuffer2

TensorBuffer2 是张量底层的数据,也许联系到 PyTorch 的 data 或者 storage 可以更好的理解。

代码语言:javascript
复制
class TensorBuffer2 {
 public:
  virtual ~TensorBuffer2() {}
  virtual bool allocated() const = 0;
  virtual void *get_ptr() = 0;
};
4.1.2 Tensor2

这就对应了TF或者PyTorch的张量。

代码语言:javascript
复制
template <typename T>
class Tensor2 {
  std::vector<size_t> dimensions_;
  size_t num_elements_;
  std::shared_ptr<TensorBuffer2> buffer_;
}

成员函数我们选介绍两个如下:

代码语言:javascript
复制
static Tensor2 stretch_from(const TensorBag2 &bag) {
  return Tensor2(bag.dimensions_, bag.buffer_);
}

TensorBag2 shrink() const {
  return TensorBag2(dimensions_, buffer_, TensorScalarTypeFunc<T>::get_type());
}

具体如下:

4.1.3 Tensors2

Tensors2 就是 Tensor2 的一个vector。

代码语言:javascript
复制
template <typename T> using Tensors2 = std::vector<Tensor2<T>>;
4.1.4 TensorBag2

PyTorch 之中也有一些Bag后缀名字的类,比如 nn.Embedding和nn.EmbeddingBag。当构建袋子模型时,做一个Embedding跟随Sum或是Mean常见的。对于可变长度序列,nn.EmbeddingBag 来提供了更加高效和更快速的处理方式,特别是对于可变长度序列。

在 HugeCTR,TensorBag2 可以认为是把 Tensor 放在袋子里统一处理的类。

代码语言:javascript
复制
class TensorBag2 {
  template <typename T>
  friend class Tensor2;

  std::vector<size_t> dimensions_;
  std::shared_ptr<TensorBuffer2> buffer_;
  TensorScalarType scalar_type_;
};

using TensorBags2 = std::vector<TensorBag2>;

关于 Tensor 和 Bag 的联系,可以参见下面的函数。

代码语言:javascript
复制
template <typename T>
Tensors2<T> bags_to_tensors(const std::vector<TensorBag2> &bags) {
  Tensors2<T> tensors;
  for (const auto &bag : bags) {
    tensors.push_back(Tensor2<T>::stretch_from(bag));
  }
  return tensors;
}

template <typename T>
std::vector<TensorBag2> tensors_to_bags(const Tensors2<T> &tensors) {
  std::vector<TensorBag2> bags;
  for (const auto &tensor : tensors) {
    bags.push_back(tensor.shrink());
  }
  return bags;
}
4.1.5 SparseTensor

SparseTensor 是 Sparse 类型的张量,这是3.2 版本加入的,目的是为了统一处理CSR格式,或者说是统一处理稀疏矩阵,可以有效存储和处理大多数元素为零的张量。后续在读取数据到GPU时候会有分析。我们对比一下 CSR 格式,就可以看出来其内部机制就对应了CSR 的 rowoffset 和 value。其具体定义如下:

代码语言:javascript
复制
template <typename T>
class SparseTensor {
  std::vector<size_t> dimensions_;
  std::shared_ptr<TensorBuffer2> value_buffer_;
  std::shared_ptr<TensorBuffer2> rowoffset_buffer_;
  std::shared_ptr<size_t> nnz_;  // maybe size_t for FixedLengthSparseTensor
  size_t rowoffset_count_;
};

示意图如下:

我们从中找出一个例子看看。因为只是用来存储slot里的sparse key,所以没有列号,因为一个slot里的sparse key可以直接顺序存储。

代码语言:javascript
复制
* For example data:
*   4,5,1,2
*   3,5,1
*   3,2
* Will be convert to the form of:
* row offset: 0,4,7,9
* value: 4,5,1,2,3,5,1,3,2

对应下图:

成员函数介绍如下:

代码语言:javascript
复制
static SparseTensor stretch_from(const SparseTensorBag &bag) {
  return SparseTensor(bag.dimensions_, bag.value_buffer_, bag.rowoffset_buffer_, bag.nnz_,
                      bag.rowoffset_count_);
}

SparseTensorBag shrink() const {
  return SparseTensorBag(dimensions_, value_buffer_, rowoffset_buffer_, nnz_, rowoffset_count_,
                         TensorScalarTypeFunc<T>::get_type());
}
PyTorch

PyTorch 有 sparse_coo_tensor 可以实现类似的功能。PyTorch 支持不同layout的张量,大家可以从 torch/csrc/utils/tensor_layouts.cpp 找到,比如 at::Layout::Strided,at::Layout::Sparse,at::Layout::SparseCsr,at::Layout::Mkldnn 等等,这些对应了不同的内存布局模式。

使用稀疏张量时候,提供一对 dense tensors:一个value张量,一个二维indice张量,也有其他辅助参数。

代码语言:javascript
复制
>>> i = [[1, 1]]
>>> v =  [3, 4]
>>> s=torch.sparse_coo_tensor(i, v, (3,))
>>> s
tensor(indices=tensor([[1, 1]]),
       values=tensor(  [3, 4]),
       size=(3,), nnz=2, layout=torch.sparse_coo)
TensorFlow

TensorFlow 也有 SparseTensor 类型来表示多维稀疏数据。一个 SparseTensor 使用三个稠密张量来表示:

  • indices 表示稀疏张量的非零元素坐标。
  • values 则对应每个非零元素的值。
  • shape 表示本稀疏张量转换为稠密形式后的形状。

比如下面代码:

代码语言:javascript
复制
indices = tf.constant([[0, 0], [1, 1], [2,2]], dtype=tf.int64)
values = tf.constant([1, 2, 3], dtype=tf.float32)
shape = tf.constant([3, 3], dtype=tf.int64)
sparse = tf.SparseTensor(indices=indices,
                            values=values,
                            dense_shape=shape)
dense = tf.sparse_tensor_to_dense(sparse, default_value=0)
with tf.Session() as session:
    sparse, dense = session.run([sparse, dense])
    print('Sparse is :\n', sparse)
    print('Dense is :\n', dense)

打印出来如下:

代码语言:javascript
复制
Sparse is :
 SparseTensorValue(indices=array([[0, 0],
       [1, 1],
       [2, 2]]), values=array([1., 2., 3.], dtype=float32), dense_shape=array([3, 3]))
Dense is :
 [[1. 0. 0.]
 [0. 2. 0.]
 [0. 0. 3.]]
4.1.6 SparseTensorBag

这个类似 TensorBag 的功能,具体如下:

代码语言:javascript
复制
class SparseTensorBag {
  template <typename T>
  friend class SparseTensor;

  std::vector<size_t> dimensions_;
  std::shared_ptr<TensorBuffer2> value_buffer_;
  std::shared_ptr<TensorBuffer2> rowoffset_buffer_;
  std::shared_ptr<size_t> nnz_;
  size_t rowoffset_count_;
  TensorScalarType scalar_type_;

  SparseTensorBag(const std::vector<size_t> &dimensions,
                  const std::shared_ptr<TensorBuffer2> &value_buffer,
                  const std::shared_ptr<TensorBuffer2> &rowoffset_buffer,
                  const std::shared_ptr<size_t> &nnz, const size_t rowoffset_count,
                  TensorScalarType scalar_type)
      : dimensions_(dimensions),
        value_buffer_(value_buffer),
        rowoffset_buffer_(rowoffset_buffer),
        nnz_(nnz),
        rowoffset_count_(rowoffset_count),
        scalar_type_(scalar_type) {}

 public:
  SparseTensorBag() : scalar_type_(TensorScalarType::None) {}
  const std::vector<size_t> &get_dimensions() const { return dimensions_; }
};
4.1.7 向量类

以下是两个向量类,用来方便用户使用。

代码语言:javascript
复制
using TensorBags2 = std::vector<TensorBag2>;

template <typename T>
using SparseTensors = std::vector<SparseTensor<T>>;

4.2 内存

我们接下来看看一些内存相关类。

4.2.1 Allocator

首先看看如何为tensor等变量分配内存。

4.2.1.1 HostAllocator

HostAllocator 作用是在host之上管理内存。

代码语言:javascript
复制
class HostAllocator {
 public:
  void *allocate(size_t size) const { return malloc(size); }
  void deallocate(void *ptr) const { free(ptr); }
};

后面几个实现都是调用了CUDA函数来进行内存分配,比如 cudaHostAlloc,有兴趣读者可以深入学习。

4.2.1.2 CudaHostAllocator

调用CUDA方法在主机上分配内存

代码语言:javascript
复制
class CudaHostAllocator {
 public:
  void *allocate(size_t size) const {
    void *ptr;
    CK_CUDA_THROW_(cudaHostAlloc(&ptr, size, cudaHostAllocDefault));
    return ptr;
  }
  void deallocate(void *ptr) const { CK_CUDA_THROW_(cudaFreeHost(ptr)); }
};
4.2.1.3 CudaManagedAllocator

cudaMallocManaged 分配旨在供主机或设备代码使用的内存,算是一种统一分配内存的方法。

代码语言:javascript
复制
class CudaManagedAllocator {
 public:
  void *allocate(size_t size) const {
    void *ptr;
    CK_CUDA_THROW_(cudaMallocManaged(&ptr, size));
    return ptr;
  }
  void deallocate(void *ptr) const { CK_CUDA_THROW_(cudaFree(ptr)); }
};
4.2.1.4 CudaAllocator

该类是在设备上分配内存。

代码语言:javascript
复制
class CudaAllocator {
 public:
  void *allocate(size_t size) const {
    void *ptr;
    CK_CUDA_THROW_(cudaMalloc(&ptr, size));
    return ptr;
  }
  void deallocate(void *ptr) const { CK_CUDA_THROW_(cudaFree(ptr)); }
};
4.2.2 GeneralBuffer2

分析完如何分配内存,我们接下来看看如何封装内存,具体通过 GeneralBuffer2 完成的。GeneralBuffer2 可以认为是一个对大段内存的统一封装,具体在其上可以有若干Tensor。

4.2.2.1 定义

这里都忽略了成员函数,内部类也忽略了成员函数。

  • allocator :具体内存分配器,也区分在GPU分配还是CPU分配。
  • ptr_ :指向分配的内存;
  • total_size_in_bytes_ :内存大小;
  • reserved_buffers_ :前期预留buffer,后续会统一分配;

具体内部类为:

  • BufferInternal 是接口。
  • TensorBufferImpl 是 Tensor2 对应的buffer实现。
  • BufferBlockImpl 则是在构建网络时候会用到。

具体代码如下:

代码语言:javascript
复制
template <typename Allocator>
class GeneralBuffer2 : public std::enable_shared_from_this<GeneralBuffer2<Allocator>> {
  
  class BufferInternal {
   public:
    virtual ~BufferInternal() {}
    virtual size_t get_size_in_bytes() const = 0;
    virtual void initialize(const std::shared_ptr<GeneralBuffer2> &buffer, size_t offset) = 0;
  };

  class TensorBufferImpl : public TensorBuffer2, public BufferInternal {
    size_t size_in_bytes_;
    std::shared_ptr<GeneralBuffer2> buffer_;
    size_t offset_;
  };

  template <typename T>
  class BufferBlockImpl : public BufferBlock2<T>, public BufferInternal {
    size_t total_num_elements_;
    std::shared_ptr<TensorBufferImpl> buffer_impl_;
    Tensor2<T> tensor_;
    bool finalized_;
    std::vector<std::shared_ptr<BufferInternal>> reserved_buffers_;
  };

  Allocator allocator_;
  void *ptr_;
  size_t total_size_in_bytes_;
  std::vector<std::shared_ptr<BufferInternal>> reserved_buffers_;
}
4.2.2.2 TensorBufferImpl

就是指向了一个 GeneralBuffer2,然后设定了自己的offset和大小。

代码语言:javascript
复制
void initialize(const std::shared_ptr<GeneralBuffer2> &buffer, size_t offset) {
  buffer_ = buffer;
  offset_ = offset;
}
4.2.2.2 BufferBlockImpl 关键函数

BufferBlockImpl 和 TensorBufferImpl 可以来比较一下。

其中,BufferBlock2 是 BufferBlockImpl 的接口类。

代码语言:javascript
复制
template <typename T>
class BufferBlock2 {
 public:
  virtual ~BufferBlock2() {}
  virtual void reserve(const std::vector<size_t> &dimensions, Tensor2<T> *tensor) = 0;
  virtual Tensor2<T> &as_tensor() = 0;
};

BufferBlockImpl 是一组连续的 Tensor,某些特定的实现需要连续的内存,比如权重。

代码语言:javascript
复制
std::shared_ptr<BufferBlock2<float>> train_weight_buff = blobs_buff->create_block<float>();

// 省略其他代码......

network->train_weight_tensor_ = train_weight_buff->as_tensor();

BufferBlockImpl 多了一个reserve方法,用来预留内存空间,在此空间之上生成内部tensor。

代码语言:javascript
复制
void reserve(const std::vector<size_t> &dimensions, Tensor2<T> *tensor) override {
  if (finalized_) {
    throw std::runtime_error(ErrorBase + "Buffer block is finalized.");
  }
  size_t num_elements = get_num_elements_from_dimensions(dimensions);
  size_t size_in_bytes = num_elements * TensorScalarSizeFunc<T>::get_element_size();

  std::shared_ptr<TensorBufferImpl> buffer_impl =
      std::make_shared<TensorBufferImpl>(size_in_bytes);
  reserved_buffers_.push_back(buffer_impl);

  *tensor = Tensor2<T>(dimensions, buffer_impl);

  total_num_elements_ += num_elements;
}

initialize 会对内部进行配置

代码语言:javascript
复制
void initialize(const std::shared_ptr<GeneralBuffer2> &buffer, size_t offset) {
  size_t local_offset = 0;
  for (const std::shared_ptr<BufferInternal> &buffer_impl : reserved_buffers_) {
    buffer_impl->initialize(buffer, offset + local_offset);
    local_offset += buffer_impl->get_size_in_bytes();
  }
  reserved_buffers_.clear();

  if (!finalized_) {
    buffer_impl_ = std::make_shared<TensorBufferImpl>(
        total_num_elements_ * TensorScalarSizeFunc<T>::get_element_size());
    tensor_ = Tensor2<T>({total_num_elements_}, buffer_impl_);
    finalized_ = true;
  }
  buffer_impl_->initialize(buffer, offset);
}
4.2.2.3 GeneralBuffer2 关键函数

reserve 方法会把某一个张量对应的内存需求用 TensorBufferImpl 的形式记录在reserved_buffers_之中,然后生成这个张量,而且就是用TensorBufferImpl 生成。

代码语言:javascript
复制
template <typename T>
void reserve(const std::vector<size_t> &dimensions, Tensor2<T> *tensor) {
  if (allocated()) {
    throw std::runtime_error(ErrorBase + "General buffer is finalized.");
  }

  size_t size_in_bytes =
      get_num_elements_from_dimensions(dimensions) * TensorScalarSizeFunc<T>::get_element_size();

  std::shared_ptr<TensorBufferImpl> buffer_impl =
      std::make_shared<TensorBufferImpl>(size_in_bytes);
  reserved_buffers_.push_back(buffer_impl);

  *tensor = Tensor2<T>(dimensions, buffer_impl);
}

create_block 会针对BufferBlock2进行创建。

代码语言:javascript
复制
template <typename T>
std::shared_ptr<BufferBlock2<T>> create_block() {
  if (allocated()) {
    throw std::runtime_error(ErrorBase + "General buffer is finalized.");
  }
  std::shared_ptr<BufferBlockImpl<T>> block_impl = std::make_shared<BufferBlockImpl<T>>();
  reserved_buffers_.push_back(block_impl);
  return block_impl;
}

allocate 会遍历注册的 BufferInternal,累积其总大小,最后调用 allocator_ 进行分配内存。

代码语言:javascript
复制
void allocate() {
  if (ptr_ != nullptr) {
    throw std::runtime_error(ErrorBase + "Memory has already been allocated.");
  }

  size_t offset = 0;
  for (const std::shared_ptr<BufferInternal> &buffer : reserved_buffers_) {
    // 对 BufferInternal(比如TensorBufferImpl)内部进行配置
    buffer->initialize(this->shared_from_this(), offset);
    size_t size_in_bytes = buffer->get_size_in_bytes();
    if (size_in_bytes % 32 != 0) {
      size_in_bytes += (32 - size_in_bytes % 32);
    }
    offset += size_in_bytes;
  }
  reserved_buffers_.clear();
  total_size_in_bytes_ = offset;

  if (total_size_in_bytes_ != 0) {
    ptr_ = allocator_.allocate(total_size_in_bytes_);
  }
}
4.2.4 小结

至此,Tensor的逻辑拓展一下:

  • TensorBufferImpl 的 buffer 是GeneralBuffer2;
  • GeneralBuffer2 的 ptr 是由CudaAllocator在GPU之中分配的;GeneralBuffer2 可以认为是一个对大段内存的统一封装,在其上可以有若干Tensor。这些Tensor先reserve内存,然后统一分配。
  • TensorBufferImpl 的 offset_ 就指向了 GeneralBuffer2 的 ptr 之中具体的某一个内存偏移;
  • BufferBlockImpl 用来实现一个连续的Tensor内存。

如果还有另外一个 Tensor2,则其 TensorBufferImpl.offset 会指向 GPU内存的另外一个offset,比如下面有两个张量,Tensor 1 和 Tensor 2。

0xFF 参考

https://developer.nvidia.com/blog/introducing-merlin-hugectr-training-framework-dedicated-to-recommender-systems/

https://developer.nvidia.com/blog/announcing-nvidia-merlin-application-framework-for-deep-recommender-systems/

https://developer.nvidia.com/blog/accelerating-recommender-systems-training-with-nvidia-merlin-open-beta/

HugeCTR源码阅读

embedding层如何反向传播

https://web.eecs.umich.edu/~justincj/teaching/eecs442/notes/linear-backprop.html

稀疏矩阵存储格式总结+存储效率对比:COO,CSR,DIA,ELL,HYB

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00 摘要
  • 0x01 回顾
  • 0x02 数据集
    • 2.1 Norm
      • 2.1.1 数据文件
      • 2.1.2 文件列表
    • 2.2 Raw
      • 2.3 Parquet
      • 0x03 CSR 格式
        • 3.1 什么是CSR
          • 3.2 HugeCTR 之中的CSR
            • 3.3 操作类
              • 3.3.1 定义
              • 3.3.2 构造函数
              • 3.3.3 生成新行
              • 3.3.4 插入数据
          • 0x04 基础数据结构
            • 4.1 张量
              • 4.1.1 TensorBuffer2
              • 4.1.2 Tensor2
              • 4.1.3 Tensors2
              • 4.1.4 TensorBag2
              • 4.1.5 SparseTensor
              • 4.1.6 SparseTensorBag
              • 4.1.7 向量类
            • 4.2 内存
              • 4.2.1 Allocator
              • 4.2.2 GeneralBuffer2
          • 0xFF 参考
          相关产品与服务
          GPU 云服务器
          GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档