框架层面的两个主要目标在于:
- 对用户层,暴露统一的接口,以便于不同平台上可以共用一套应用源码。
- 通过合理地设置回调函数,将计算过程中需要进行访存的情况传递到不同平台的后端,从而使不同平台上的后端得以被开发。
其中在这一期开发过程中,最重要的是:
- 向量迭代器
- 矩阵-向量迭代器
- 矩阵-矩阵迭代器
这些迭代器都是接收Functor/LAMBDA或者函数作为算子参数,算子接收几个固定的参数指定要计算的位置,以及几个不定参数指定用于计算的数据。
迭代器在给定的拓扑下(向量迭代器无拓扑,矩阵-向量单拓扑,矩阵-矩阵三个拓扑)实现对应的循环代码并在其中调用算子,从而简化这些计算过程的代码编写。
在性能方面主要是尽可能地利用编译期推导和函数内联,减少可变基址和函数内联的情况,这样可以在后期尽可能提高神威从核上的性能。
项目本身对功能的耦合是比较紧密的,为了更好地分解功能,我们主要将需要进行的操作分为两类,一类是迭代器中的操作,显然是在迭代器中进行实现,但是关于逐个数据集的操作为了方便可以在DataSet
类中实现并集中调用。
对于拓扑来说,最关键的问题是需要承载部分inspector数据,例如染色分块,或者矩阵乘的结果映射等,后期考虑用<观察者目的, 无类型指针>
的二元组统一管理。
目前UNAT++的模块划分如下图所示:
目前对应的源码和目录的组织为:
include/unatpp/list.hpp =>
include/unatpp/topology.hpp => struct Topology
include/unatpp/dataset.hpp => struct Dataset
include/unatpp/operator-helper.hpp => 用于解参数包
include/unatpp/cxx11compat.hpp => 解决C++11 14不支持remove_reference_t
include/unatpp/iterate-vector.hpp => iterateV
include/unatpp/iterate-matvec.hpp => iterateMV
include/unatpp/iterate-matmat.hpp => iterateMM
include/unatpp/iterate.hpp => iterate
include/unatpp.hpp => #include ^^^
目前尚未进行并行优化,但是在框架开发过程中我们也考虑了接口调用硬件相关功能的可能性,决定抽象DataSet
的make_buffer
、enter_range
和leave_range
系列回调,在计算过程中通过对应的回调完成缓冲区申请和数据传输。
这种情况下,对于普通的向量迭代,其过程类似:
对于矩阵-向量迭代,这个过程更加复杂,因为该接口开始有DataPosi
的区分,在make_buffer
函数中我们将向DataSet
传入BLKSZV
和BLKSZM
两个参数,代表为向量和矩阵开辟的缓冲区大小,DataSet
中基于自己的DataPosi
确定返回的缓冲区大小,在enter_range
和leave_range
中,DataSet
基于传入的DataPosi
是否匹配决定是否执行对应的操作。过程类似:
矩阵-矩阵迭代的优化思路尚未确定,比较可能的一个策略是特化DataSet
,行列和湮灭维度可以较正常地处理,结果矩阵按行区间重用,左矩阵按结果矩阵的行区间分块后按列存储,右矩阵每次取一行数据。目前只实现了简单的迭代器。
调用UNAT++中的接口,最简单的方式是包含unatpp.h
,然后利用iterate
来实现迭代。
这里对iterate
接受的参数类型进行了一定的限制。
迭代接口重载为三种形式:
template<typename ArrayOpt, typename ...Ts>
void iterateV(int n, ArrayOpt &&opt, Ts* ...args); //向量迭代,尾部参数为指针
template <typename ArrayOpt, int ...DPs, int ...CPs, typename... TPs>
void iterateV(int n, ArrayOpt &&opt, DataSet<DPs, CPs, TPs>... sets); //向量迭代,尾部参数为DataSet
template<typename MVOpt, typename ...Ts>
void iterateMV(Topology &topo, MVOpt &&opt, Ts* ...args); //矩阵-向量迭代,尾部参数为指针
template<typename MVOpt, int ...DPs, int ...CPs, typename... TPs>
void iterateMV(Topology &topo, MVOpt &&opt, DataSet<DPs, CPs, TPs>... sets); //矩阵-向量迭代,尾部参数为DataSet
template<typename MMOpt, typename ...Ts>
void iterateMM(Topology &topo, Topology <opo, Topology &rtopo, MMOpt &opt, Ts* ...args); //矩阵-矩阵迭代,尾部参数为指针
template<typename MMOpt, int ...DPs, int ...CPs, typename... TPs>
void iterateMM(Topology &topo, Topology <opo, Topology &rtopo, MMOpt &opt, DataSet<DPs, CPs, TPs>... sets); //矩阵-矩阵迭代,尾部参数为dataset
//统一重载
template<typename ...Ts>
void iterate(Ts& ...args); //向量迭代
template<typename ...Ts>
void iterate(Topology &topo, Ts& ...args); //向量迭代
template<typename ...Ts>
void iterate(Topology &topo, Topology <opo, Topology &rtopo, Ts& ...args); //向量迭代
此处考虑放弃尾部参数为DataSet
的接口,因为我认为尾部参数为指针更加灵活。
此处应考虑利用static_assert
完成提前报错,防止报错信息太长。
目前由于划分算法尚未确定,Topology
的具体实现尚未确定,计划采用COO+CSR/CSC的形式来存储,即:
struct Topology {
enum topology_order {
CSR,
CSC,
UNO
} order;
int nrow, ncol, nent;
int *spanst, *spaned;//[nrow/ncol]
int *col, *row;//[nent]
//其他辅助计算的数据...
matmat_index *matmat;
blocks *blks;
};
Topology
进行按行/列的排序,并据此填充spanst
和spaned
,使得该结构既可以当作COO访问,也可以当做CSR或CSC访问。目前的计算均在CSR格式上进行。
为了实现矩阵-矩阵迭代,实现了Topology operator *(Topology &l, Topology &r)
接口获取两矩阵相乘后的拓扑。
Topology
实现了限制子矩阵大小的分块。
所以目前Topology
的接口包括:
/*把矩阵划分为若干个不超过rowblk*colblk的子矩阵,colalign决定了列的划分是否按照colblk对齐*/
Topology::partblks(int rowblk, int colblk, bool colalign = false);
/*计算矩阵相乘后的拓扑*/
static Topology operator*(Topology <op, Topology &rtop);
/*
为矩阵-矩阵迭代进行分块
结果矩阵分块为rowblk*colblk
左矩阵分块为rowblk*dumblk
右矩阵分块为ndum*colblk
*/
static void matmat_copart(Topology &topo, Topology <opo, Topology &rtopo, int rowblk, int colblk, int dumblk);
迭代器回调分为两类事件:
DataSet.xxx_make_buffer<N...>()
:进入迭代器时,为DataSet
分配指定长度的缓冲区,xxx
可能为vec
、matvec
或者matmat
。DataSet.enter_range<DataPosi>(buffer, start, end)
/DataSet.leave_range<DataPosi>(buffer, start, end)
:迭代器进入新的一块计算时,进行数据读取的回调,DataSet
可以根据迭代器传入的DataPosi
是否匹配决定是否执行回调。
缓冲区分为一维缓冲和二维缓冲,二维缓冲仅用于矩阵-矩阵迭代时的结果矩阵。
/*一维缓冲*/
template <int N> struct buffer1d {
T dat[N];
int start;
/*取数据*/
void fetch(T *from, int start, int end);
/*写回数据*/
void write(T *to, int start, int end);
/*给出一个引用缓冲区内数据,但伪装成DataSet的类,且在数据范围内与原DataSet可以使用相同的下标*/
DataSet pseudo_dataset();
};
/*二维缓冲实现是多个一维缓冲而不是真正的多维缓冲,因为CSR不同行的相同列未必对齐*/
/*二维缓冲兼容一维缓冲的调用,并且在N==0时按照一维缓冲的逻辑执行,从而避免另开一个类*/
/*后续开发可以逐步废弃一维缓冲*/
template <int N, int M> struct buffer2d {
T dat[N][M];
int start[N];
/*这里的which是动用二维缓冲的第几行*/
void fetch(int which, T *from, int start, int end);
void write(int which, T *to, int start, int end);
void fetch(T *from, int start, int end);
void write(T *to, int start, int end);
DataSet pseudo_dataset(int which = 0);
};
整数序列是一个C++14特性,考虑到尚不决定放弃C++11支持,这里决定重新造轮子。
内部使用模板递归实现,暴露的主要接口为:
template<int ...Is> struct seq_t;
template<int A> genseq_t; //等价于seq_t<0,1,...A>
template<int A, int B> genseq_t; //等价于seq_t<A,A+1,...,B>
参数列表使用变参结构体实现,暴露的接口如下所示:
template<typename ...Ts>
struct arg_list{
/*Us型指针转成参数列表对应的DataSet*/
template<typename ...Us>
static constexpr std::tuple<Ts...> convert_pointers(std::tuple<Us...> args);
/*参数列表取一个区间,用于剥离算子参数中的下标*/
template<int START, int END=sizeof...(Ts)>
static constexpr decltype(pick(genseq_t<START, END>{})) sublist();
/*参数列表长度和对应的整数序列*/
static constexpr int nargs = sizeof...(Ts);
static constexpr genseq_t<nargs> iseq{};
/*计算特定DataSet的数据类型大小之和,后续可以用于智能的缓冲区分配*/
/*入口函数,DP=-1或者CP=-1代表匹配任意DataPosi或者CopyProp*/
template<int DP, int CP>
static constexpr size_t datatype_size();
};
/*从算子中提取参数列表*/
template<int I=0, typename T>
constexpr /*arg_list<T::operator()的参数列表>*/ get_args(T &&v);
/* 这个必须用宏,把参数包的void返回值函数调用展开执行*/
/* 无非是选择使用数组初始化器或者函数参数展开,数组初始化器的优势是保序*/
#define runall(x) { int unused[] = {(x, 0)...}; }
矩阵-向量接口临时提供part_blks(int rowblk, int colblk)
函数进行分块,实现对CSR结构的子分。
目前暂定的分块格式为:
struct block {
int rowoff, rowcnt;//行偏移/行数
int coloff, colcnt;//列偏移/列数
int *rowst, *rowed;//行起始/结束
};
void partblks(int rowblk, int colblk);
对拓扑来说,期望在按照先行后列的排序后执行分块,则分块矩阵的每行可以对应到原矩阵对应行的一个区间,这是比较优秀的特性,可以减少对矩阵数据的拷贝,也减少对DataSet的重排。
当前的规划中可能会针对iterateV
、iterateMV
和iterateMM
设置不同的回调,因为尚未确定矩阵迭代中的分块数据格式,这里计划通过添加前后缀来实现,回调的函数名类似场景_回调名_数据位置
,例如:矩阵乘矩阵中的左矩阵进入块的回调将被命名为matmat_enter_block_lmatrix()
。具体将添加的前后缀包括:
场景 | 数据位置 | 回调名 |
---|---|---|
vec |
vec_xxx_block |
|
matvec |
ROW |
matvec_xxx_row |
matvec |
COL |
matvec_xxx_col |
matvec |
MATRIX |
matvec_xxx_matrix |
matmat |
ROW |
matmat_xxx_row |
matmat |
COL |
matmat_xxx_col |
matmat |
DUM |
matmat_xxx_dum |
matmat |
LMATRIX |
matmat_xxx_lmatrix |
matmat |
RMATRIX |
matmat_xxx_rmatrix |
matmat |
MATRIX |
matmat_xxx_matrix |
矩阵-矩阵接口:
左右矩阵构建
基于左右矩阵构建结果矩阵拓扑,值得实现一个函数
拓扑在矩阵-矩阵接口中确实需要进行分类,因为左右和结果可能访问顺序比较不一样。
//c[i][k] += a[i][j]*b[j][k];
//c[i][...] += a[i][j]*b[j][...]
// vector<pair<int,int>> result_elems;
struct matmat_index {
int ent, lent, rent, row, col, dum;
};
vector<matmat_index> relation;
Topology operator*(Topology &l, Topology &r) {
for (int i = 0; i < ltopo.nentry; i ++) {
int r = ltopo.r[i];
int c = ltopo.c[i];
for (int j = rtopo.rowst[c]; j < rtopo.rowed[c]; j ++) {
// result_elems.push_back(std::make_pair(r, rtopo.c[j]));
relation.push_back(matmat_index{-1, i, j, r, rtopo.col[j], c});
}
}
//排relation->row, col
//填充CSR/COO
//for (int i = 0; i < relation.size(); i ++) {
// if (relation[i].row != relation[i-1].row || relation[i].col != relation[i-1].col) cur++;
// relation[i].ent = cur;
//}
ptopo.sort_unique(relation);
}
for (int i = 0; i < ptopo.relation.size(); i ++) {
matmat_index mmi = ptopo.relation[i];
opt(mmi.ent, mmi.lent, mmi.rent, mmi.row, mmi.col, mmi.dum, sets...);
}
#pragma once
include/unatpp/topology.hpp => struct Topology
include/unatpp/dataset.hpp => struct Dataset
include/unatpp/iterate-vector.hpp => iterateV()
include/unatpp/iterate-matvec.hpp => iterateMV()
include/unatpp/iterate-matmat.hpp => iterateMM()
include/unatpp.hpp => #include ^^^
example/example-vector.cpp
example/example-matvec.cpp
example/example-matmat.cpp
DataSet {
template<int N>
struct vec_buffer {
T dat[N];
};
struct matvec_buffer {
Dataset &dat;
};
struct matmat_buffer {
Dataset &dat;
};
vec_make_buffer<N>() {return vec_buffer<N>{};};
matvec_make_buffer() {return matvec_buffer{*this};};
matmat_make_buffer() {return matvec_buffer{*this};};
vec_enter_block
vec_exit_block
matvec_enter_matrix(){}
matvec_enter_row(){}
matvec_enter_col(){}
matmat_enter_lmatrix(){}
matmat_enter_rmatrix(){}
matmat_enter_matrix(){}
matmat_enter_row(){}
matmat_enter_col(){}
matmat_enter_dum(){}
}
``` -->