分布式矩阵
1. mlib.linalg.distributed包
矩阵计算是很多科学计算的重要步骤,而分布式矩阵存储则是分布式计算的基础。根据不同的计算需求,需要将用于计算的矩阵进行拆分,利用map-reduce的思想将整块矩阵的计算map成子矩阵的操作,从而得以在一个矩阵的计算步中,矩阵各部分在不同的计算单元上同步进行,最终将结果reduce汇总,从而得到计算结果。这样的矩阵分块计算针对大型矩阵运算有着很好的加速效果,而不论是GPU常见的Cuda编程还是CPU常见的MPI编程、Hadoop或Spark编程,都需要矩阵分块存储运算这一关键功能。
Spark的mlib库所使用的分布式矩阵,均在 spark.mlib.linalg.distributed 包内,接下来的内容就将对distributed包内的各种不同分块矩阵形式的创建和操作进行 源码级 的详细介绍。
2. DistributedMatrix特质
DistributedMatrix特质是distributed包中仅有的一个特质,也就是类似Java中的接口类,而这个类也十分简单,只定义了两个返回行列数的抽象类和一个返回Breeze库中DenseMatrix类的toBreeze私有抽象类,这些都将被接下来介绍的数个分布式矩阵存储形式类继承并实现。
3. BlockMatrix类
BlockMatrix是分布式矩阵存储中最常用的类型,它将矩阵按行列分块存储,而这样的存储形式对常见的矩阵运算都能提供很好的支持。我们先来看一下BlockMatrix的两个构造函数:
// BlockMatrix主构造函数
new BlockMatrix(blocks: RDD[((Int, Int), Matrix)], rowsPerBlock: Int, colsPerBlock: Int, nRows: Long, nCols: Long)
// BlockMatrix辅助构造函数
new BlockMatrix(blocks: RDD[((Int, Int), Matrix)], rowsPerBlock: Int, colsPerBlock: Int)
可以看到,BlockMatrix的主构造函数和辅助构造函数都需要以一个 RDD[((Int, Int), Matrix)] 类型的矩阵数据输入和两个 Int 型的矩阵信息输入,而主构函数增加了两个 Long 型矩阵整体行列数的定义,我们看一下主构造函数的文档解释:
blocks 所需的RDD类型 RDD[((Int, Int), Matrix)] 是原矩阵的一个子分块矩阵,第一个两个Int构成的元组分别标示了该子分块矩阵在原矩阵总分块中的行列标,而第二个Matrix类则是可以向下兼容DenseMatrix和SparseMatrix的子分块矩阵。【DenseMatrix与SparseMatrix的创建与操作】
由此可见,BlockMatrix的存储如图所示:
这样的矩阵分块形式经常会存在一个问题,当分到最后一行和最后一列时,行列数无法满足给定的 rowsPerBlock 和 colsPerBlock 。而从官方文档中可以看出,构造函数的这两个数值允许在最后一行和最后一列的子矩阵中不满足,从而在创建时就不用担心矩阵分块所产生的不满矩阵问题。
了解完构造函数,就让我们看看BlockMatrix都有哪些可用的操作方法:
1. add(other: BlockMatrix): BlockMatrix
2. subtract(other: BlockMatrix): BlockMatrix
相加 / 相减方法【传入另一个BlockMatrix,返回BlockMatrix为两个矩阵之和 / 之差】:是的是相加相减!连DenseMatrix和SparseMatrix这两个矩阵基本存储类都没有的相加方法竟然在分布式矩阵中创建了操作方法。(DenseMatrix和SparseMatrx在编写时想实现矩阵相加还是有多种方法的,这个会在之后的文章中讲述)
3. blocks: RDD[((Int, Int), Matrix)]
提取子矩阵RDD方法【直接调用,返回构成BlockMatrix的子矩阵RDD】
4. cache(): BlockMatrix.this.type
缓存操作【直接调用,将该分块矩阵进行缓存】:
缓存这个概念比较特殊,我需要单独拿出来讲一下。
- 首先,为什么要缓存? 这个跟Spark的运行机制有关。分布式系统在节点计算上一定要设计足够的容错机制,避免个别节点的问题导致整个计算任务的计算失败,所以Spark在将task分配到executor执行时,会将对RDD的操作,也就是RDD间的依赖关系进行保存,特别针对窄依赖而言,由于其可以在多个计算节点上互不相干地并行执行,所以通过对窄依赖的保存可以在某一节点执行task失败时,通过窄依赖重新计算RDD,甚至交由其它节点代替执行。
但是如果这个task内的RDD算子计算十分复杂或步骤较多,计算开销巨大,则在这个task中执行完某些计算开销巨大的RDD算子之后对RDD进行缓存,则可以在之后某个算子执行失败后的重新计算中,从缓存过的RDD开始,而不必从头计算