Spark上的DL4J:操作指南
此页面包含许多用于常见分布式训练任务的操作指南。注意,对于构建数据管道的指南,请参见这里。
在阅读这些指南之前,请确保你已经阅读了有关DL4J spark训练的介绍指南。
在训练指南前之前
- 如何用Maven构建用于通过Spark submit训练的uber jar
- 如何在Spark上使用GPU进行训练
- 如何在主节点上使用CPU,在工作机上用GPU
- 如何配置Spark的内存设置
- 如何为工作机配置垃圾收集
- 如何与DL4J和ND4J一起使用Kryo序列化
- 如何使用YARN与GPU
- 如何配置Spark位置配置
训练指南期间和之后
- 如何配置编码阈值
- 如何进行分布式测试集评估
- 如何保存(加载)Spark训练的神经网络
- 如何执行分布式推理
问题及故障排除指南
- 如何调试常见的Spark依赖问题(NoClassDefFoundExcption等)
- 如何修复“Error querying NTP server”错误
- 如何安全地缓存RDD[INDArray]和RDD[DataSet ]
- 在Amazon Elastic MapReduce上修复libgomp问题
- Ubuntu 16.04的失败训练(Ubuntu bug可能影响DL4J Spark用户)
训练之前 操作指南
如何用Maven构建用于通过Spark submit训练的uber jar
当向集群提交训练作业时,典型的工作流程是构建提交给Spark submit的“uber-jar”。uber-jar是包含运行作业所需的所有依赖项(库、类文件等)的单个JAR文件。注意,Spark submit是随Spark发行版一起提供的脚本,用户为了开始执行Spark作业,向其提交作业(以JAR文件的形式)。
本指南假定你已经把代码设置为在 Spark上训练神经网络。
步骤1:决定所需的依赖。
与用DL4J和ND4J的单机训练有很多重叠。例如,对于单机训练和Spark训练,你都应该包括标准的DL4J依赖集,例如:
- deeplearning4j-core
- deeplearning4j-spark
- nd4j-native-platform (仅用于CPU训练)
此外,你还需要包括DL4J的Spark模块,dl4j-spark_2.10或dl4j-spark_2.11。这个模块对于DL4J Spark作业的开发和执行都是必需的。小心使用与集群匹配的spark版本——对于Spark版本(Spark 1 vs.Spark 2)和Scala版本(2.10vs.2.11)。如果这些不匹配,你的作业可能会在运行时失败。
依赖示例: Spark 2, Scala 2.11:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>dl4j-spark_2.11</artifactId>
<version>1.0.0-beta2_spark_2</version>
</dependency>
依赖示例, Spark 1, Scala 2.10:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>dl4j-spark_2.10</artifactId>
<version>1.0.0-beta2_spark_1</version>
</dependency>
注意,如果添加Spark依赖项,例如spark-core_2.11,可以将其设置为pom.xml中提供的scope(有关更多细节,请参阅Maven文档),因为Spark submit将向类路径添加Spark。在集群上执行时不需要添加此依赖项,但是如果希望在本地机器上测试或调试基于Sspark的作业,则可能需要添加此依赖项。
当在CUDA GPU上进行训练时,在添加CUDA依赖项时存在以下几种可能的情况:
案例1:集群节点在主节点和工作节点上安装了CUDA工具包
当CUDA工具包和CuDNN在集群节点上可用时,我们可以使用较小的依赖:
- 如果构建uber-jar的OS与集群的OS相同:引入 nd4j-cuda-x.x
- 如果构建uber-jar的OS与集群OS不同(即,在Windows上构建,在Linux集群上执行Spark):引入cuda-x.x-platform
- 在这两种情况中,引入x.x是CUDA版本——例如,对于CUDA 9.2,x.x=9.2。
案例2:集群节点在主节点和工作节点上没有安装CUDA工具箱
当在集群节点上没有安装CUDA/CUDNN时,我们可以按以下几点来做:
- 首先,根据上面的“案例1”引入依赖关系。
- 然后为集群操作系统的引入 “redist” javacpp-presets,如这里所述:DL4J CuDNN文档
步骤2:配置POM.xml文件以构建uber-jar
当使用Spark submit时,你将需要一个uber-jar来提交以启动和运行作业。在步骤1中配置相关依赖项之后,我们需要配置pom.xml文件以正确构建uber-jar。
我们建议你使用Maven shade插件来构建uber-jar。有用于此目的的替代工具/插件,但这些并不总是包括源JAR中的所有相关文件,例如Java的ServiceLoader机制正确运行所需的文件。(ND4J和许多其他软件库使用ServiceLoader机制)。
示例独立示例项目pom.xml文件中提供了适合于此目的的Maven shade配置:
<build>
<plugins>
<!-- Other plugins here if required -->
<!-- Configure maven shade to produce an uber-jar when running "mvn package" -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade-plugin.version}</version>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>bin</shadedClassifierName>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>org/datanucleus/**</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>reference.conf</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
步骤3:构建uber jar
最后,打开一个命令行窗口(Linux上的bash、Windows上的cmd等),只需运行mvn package -DskipTests
来为项目构建uber-jar。注意,uber-jar应该出现在<project_root>/target/<project_name>-bin.jar。一定要使用大的...-bin.jar文件,因为这是带有所有依赖项的shaded jar。
这是-你现在应该有一个uber-jar,适合提交到spark-submit 用于在spark上与CPU或NVIDA(CUDA)GPU一起训练网络。
如何在Spark上使用GPU进行训练
DL4J和ND4J支持使用NVIDA GPU的GPU加速。DL4J Spark训练也可以使用GPU来执行。
DL4J和ND4J是这样设计的:代码(神经网络配置,数据管道代码)是“后端独立的”。也就是说,你可以编写一次代码,并在CPU或GPU上执行它,只需要包括适当的后端(用于CPU的nd4j-native后端,或用于GPU的nd4j-cuda-x.x)。在这方面,在Spark上执行与在单个节点上执行没有什么不同:你只需要引入适当的ND4J后端,并确保你的机器(在本例中是主/工作节点)用CUDA库适当地设置(参见uber-jar指南 来实现无需在每个节点上安装CUDA/CUDNN情况下在CUDA上运行uber jar。)
当在GPU上运行时,有几个组件:(a)ND4J CUDA后端(nd4j-cuda-x.x依赖)(b)CUDA工具箱(c)DL4J CUDA依赖项以获得cuDNN支持(deeplearning4j-cuda-x.x)(d)cuDNN库文件
(a)和(b)都必须可用于ND4J/DL4J以使用可用的CUDA GPU运行。(c)和(d)是可选的,尽管建议获得最佳性能-NVIDIA的cuDNN库能够显著加速对多层的训练,例如卷积层(ConvolutionLayer, SubsamplingLayer, BatchNormalization等)和LSTM RNN层。
若要配置Spark作业的依赖项,请参阅上面的uber jar部分。在单个节点上配置CUDNN,请参见使用CuDNN的DL4J.
如何在主节点使用CPU,工作节点上使用GPU
在某些情况下,只使用CPU运行主节点和使用GPU的工作节点可能是有意义的。如果资源(即,可用GPU机器的数量)不受限制,那么使用相同构成的集群可能更容易:即,设置集群,以便主节点也使用GPU来执行。
假设主/驱动程序在CPU机器上执行,而工作节点在GPU机器上执行,则可以简单地引入两个后端(即,如uber-jar部分中所述,nd4j-cuda-x.x
和nd4j-native 依懒
)。
当类路径上有多个后端时,默认情况下首先尝试CUDA后端。如果无法加载,则CPU(nd4j-native)后端将被第二个加载。因此,如果驱动程序没有GPU,它应该回落到使用CPU。但是,可以通过在主/驱动器上设置BACKEND_PRIORITY_CPU
或BACKEND_PRIORITY_GPU
环境变量来改变这种默认行为,如这里所述。设置环境变量的确切过程可能取决于集群管理器——Spark standalone vs.YARN vs.Mesos。请查阅每个文档,了解如何为驱动程序/主程序设置Spark作业的环境变量。
如何配置Spark的内存设置
关于DL4J和ND4J的内存和内存配置如何工作的重要背景,首先阅读ND4J/DL4J的内存管理。
Spark上的内存管理类似于用于单节点训练的内存管理:
- 堆内存使用标准的Java Xms和Xmx内存配置设置来配置。
- 使用javacpp系统属性配置堆外内存
然而,Spark上下文中的内存配置增加了一些额外的复杂性:
- 通常,对于驱动程序/主节点和工作节点,内存配置必须分开进行(有时使用不同的机制)。
- 配置内存的方法取决于集群资源管理器——Spark standalone vs.YARN vs.Mesos,等等
- 集群资源管理器默认内存设置通常不适合严重依赖堆外内存的库(如DL4J/ND4J)。
请参阅群集管理器的Spark文档:
你应该设置4件事:
- 工作节点堆内存(Xmx)——通常设置为Spark submit 的参数(例如,YARN的
--executor-memory 4g
) - 工作节点堆外内存(javacpp系统属性选项)(例如,
--conf "spark.executor.extraJavaOptions=-Dorg.bytedeco.javacpp.maxbytes=8G"
) - 驱动程序堆内存
- 驱动程序堆外内存
要注意的地方:
- 在YARN上,通常需要设置
spark.driver.memoryOverhead
和spark.executor.memoryOverhead
。默认设置对于DL4J训练来说太小了。 - 在Spark standalone 上,还可以通过修改每个节点上的conf/spark-env.sh文件来配置内存,如Spark配置文档中所述。例如,可以添加以下行来设置驱动程序的堆大小为8GB、驱动程序的堆外内存为12GB、工作节点的堆内存为12GB以及工作节点的堆外内存为18GB:
SPARK_DRIVER_OPTS=-Dorg.bytedeco.javacpp.maxbytes=12G
SPARK_DRIVER_MEMORY=8G
SPARK_WORKER_OPTS=-Dorg.bytedeco.javacpp.maxbytes=18G
SPARK_WORKER_MEMORY=12G
总的来说,这可能看起来像(对于YARN,4GB堆内存,5GB堆外内存,6GB YARN堆外内存开销):
--class my.class.name.here --num-executors 4 --executor-cores 8 --executor-memory 4G --driver-memory 4G --conf "spark.executor.extraJavaOptions=-Dorg.bytedeco.javacpp.maxbytes=5G" --conf "spark.driver.extraJavaOptions=-Dorg.bytedeco.javacpp.maxbytes=5G" --conf spark.yarn.executor.memoryOverhead=6144
如何为工作机配置垃圾收集
训练效果的一个决定因素是垃圾收集的频率。当使用默认启用的工作间(参见本文)时,减少垃圾收集的频率会很有帮助。对于简单的机器训练(和驱动程序)来说,这是很容易的:
//这将限制GC调用的频率为5000毫秒。
Nd4j.getMemoryManager().setAutoGcWindow(5000)
// 或者你可以完全禁用它
Nd4j.getMemoryManager().togglePeriodicGc(false);
但是,在驱动程序上设置此选项不会更改工作节点的设置。相反,它可以为工作节点设置如下:
new SharedTrainingMaster.Builder(voidConfiguration, minibatch)
<other configuration>
.workerTogglePeriodicGC(true) //启用定期垃圾收集…
.workerPeriodicGCFrequency(5000) //并且配置为每5秒执行一次(每5000毫秒)
.build();
默认值(从1.0.0-beta3开始)是每5秒对工节点执行一次定期的垃圾收集。
如何与DL4J和ND4J一起使用Kryo序列化
DL4J和ND4J可以利用Kryo序列化,使用适当的配置。注意,由于INDArrays的堆外内存,与其他上下文中使用Kryo相比,Kryo将提供更少的性能优势。
为了启用KRYO序列化,首先添加 ND4J KRYO依赖:
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-kryo_2.11</artifactId>
<version>${dl4j-version}</version>
</dependency>
${dl4j-version}
是 DL4J 与 ND4J的版本号
然后,在训练工作开始时,添加以下代码:
SparkConf conf = new SparkConf();
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
conf.set("spark.kryo.registrator", "org.nd4j.Nd4jRegistrator");
注意,当使用DL4J的SparkDl4jMultiLayer或SparkComputationGraph类时,如果Kryo配置不正确,将记录一个警告。
如何使用YARN与GPU
对于DL4J,CUDA GPU的唯一要求是使用适当的后端,在每个节点上安装或在uber-JAR中提供适当的NVIDIA库。对于最新版本的YARN,在某些情况下可能需要一些额外的配置——有关更多细节,请参阅YARN GPU文档。
早期版本的YARN(例如,2.7.x和类似版本)在本地不支持GPU。对于这些版本,可以使用节点标签来确保作业仅被调度到GPU的节点上。有关更多细节,请参阅Hadoop Yarn文档
注意,还需要特定于YARN的内存配置(参见内存操作)。
如何配置Spark位置配置
配置Spark locality设置是可选的配置选项,可以提高训练性能。
摘概:通过在Spark submit 配置中添加--conf spark.locality.wait=0
,可以稍微减少训练时间,通过调度网络拟合操作更快地开始来实现。
有关详细信息,请参阅链接1和链接2。
训练指南期间和之后
如何配置编码阈值
DL4J的Spark实现使用阈值编码方案在节点之间发送参数更新。这种编码方案导致小的量化消息,这显著降低了通信更新的网络成本。有关这个编码过程的更多细节,请参见技术说明页。
这个阈值编码过程引入了一个“分布式训练专用”的超参数器——编码阈值。过大阈值和过小阈值都可能导致次优性能:
- 大的阈值意味着稀少的通信-太稀少和收敛会受到影响
- 小阈值意味着更频繁的通信,但是在每个步骤中都传递更小的更改
所使用的编码阈值由ThresholdAlgorithm(阈值算法)控制。阈值算法的具体实现确定应该使用什么阈值。
DL4J的默认行为是使用AdaptiveThresholdAlgorithm,它试图将稀疏率保持在一定范围内。
- 稀疏比率定义为numValues(encodedUpdate)/numParameters - 1.0表示完全密集(所有值都传递),0.0表示完全稀疏(没有值传递)
- 较大的阈值意味着更多的稀疏值(更少的网络通信),而较小的阈值意味着更少的稀疏值(更多的网络通信)
- 默认情况下,AdaptiveThresholdAlgorithm尝试将稀疏率保持在0.01和0.0001之间。如果更新的稀疏性落在这个范围之外,则阈值要么增加要么减小,直到它落在这个范围内。
- 一个初始阈值仍然需要设置
在实践中,我们已经看到,这种自适应阈值处理工作良好。阈值算法的内置实现包括:
- AdaptiveThresholdAlgorithm
- FixedThresholdAlgorithm: 使用指定的编码阈值的固定的非自适应阈值。
- TargetSparsityThresholdAlgorithm: 一种自适应阈值算法,其以特定的稀疏性为目标,并且增加或减小阈值以尝试匹配目标。
此外,DL4J有一个ResidualPostProcessor(残差后置处理器) 接口,默认的实现是ResidualClippingPostProcessor,它每5步将残差向量剪辑到最大值为当前阈值的5倍。其动机是更新的“遗留”部分(即那些未被通信的部分)被存储在残差向量中。如果更新远远大于阈值,则可能出现我们称之为“残差爆炸”的现象——即,残差值可以继续增长到阈值的许多倍(因此将采取许多步骤来传递梯度)。残差后置处理器被用来避免这种现象。
阈值算法(和初始阈值)和ResidualPostProcessor可以设置如下:
TrainingMaster tm = new SharedTrainingMaster.Builder(voidConfiguration, minibatch)
.thresholdAlgorithm(new AdaptiveThresholdAlgorithm(this.gradientThreshold))
.residualPostProcessor(new ResidualClippingPostProcessor(5, 5))
<other config>
.build();
最后,DL4J的SharedTrainingMaster还具有编码调试模式,通过在SharedTrainingMaster构建器中设置.encodingDebugMode(true)
启用。当启用此功能时,每个工作节点将记录当前阈值、稀疏性和关于编码的各种其他统计信息。这些统计信息可以用来确定阈值是否被适当设置:例如,许多更新是阈值的十倍或几百倍可能表明阈值太低并且应该增加;在频谱的另一端,非常稀疏的更新(小于1/10000的值被传递)可以指示阈值应该降低。
如何进行分布式测试集评估
DL4J支持神经网络最标准的评估指标。有关评估的基本信息,请参见DL4J评估页面
DL4J支持的所有评估指标都可以使用Spark以分布式方式计算。
第1步:准备数据
DL4J在Spark上的评估数据与训练数据非常相似。也就是说,你可以使用:
RDD<DataSet>
或JavaRDD<DataSet>
用于单输入/输出网络评估RDD<MultiDataSet>
或JavaRDD<MultiDataSet>
用于多输入/输出网络评估RDD<String>
或JavaRDD<String>
它的每个字符串都是一个指向例如HDFS网络存储器上的DataSet/MultiDataSet (或其他基于小批量的文件格式)系列化的路径。
有关如何将数据准备为这些格式之一的详细信息,请参阅数据页。
第2步:准备你的网络
创建你的网络是很简单的。首先,使用 如何保存(和加载)在Spark上训练的神经网络 指南中的信息,将网络(MultiLayerNetwork或ComputationGraph)加载到驱动程序的内存中
然后,简单地创建你的网络:
JavaSparkContext sc = new JavaSparkContext();
MultiLayerNetwork net = <code to load network>
SparkDl4jMultiLayer sparkNet = new SparkDl4jMultiLayer(sc, cgForEval, null);
JavaSparkContext sc = new JavaSparkContext();
ComputationGraph net = <code to load network>
SparkComputationGraph sparkNet = new SparkComputationGraph(sc, net, null);
注意,你不需要配置TrainingMaster(即,上面的第三个参数为空),因为评估不使用它。
第3步:调用适当的评估方法
对于常见情况,可以在SparkDl4jMultiLayer或SparkComputationGraph上调用标准评估方法之一:
evaluate(RDD<DataSet>) //Accuracy/F1 etc for classifiers
evaluate(JavaRDD<DataSet>) //Accuracy/F1 etc for classifiers
evaluateROC(JavaRDD<DataSet>) //ROC for single output binary classifiers
evaluateRegression(JavaRDD<DataSet>) //For regression metrics
为了同时执行多个评估(比顺序执行更有效),可以使用以下方法:
IEvaluation[] evaluations = new IEvaluation[]{new Evaluation(), new ROCMultiClass()};
JavaRDD<DataSet> data = ...;
sparkNet.doEvaluation(data, 32, evaluations);
注意,一些评估方法具有带有额外参数的过载,包括:
int evalNumWorkers
- 用于评估的工作节点的数量-即,在每个节点上用于评估的网络的副本的数量(每个工作节点上最大的Spark线程数量)。对于大型网络(或有限的集群内存),你可能希望减少这个值以避免遇到内存问题。int evalBatchSize
- 执行评估时使用的小批量大小。这需要足够大,以有效地使用硬件资源,但足够小,以免耗尽内存。32-128之间的值是一个好的起点;对于较小的网络当有更多的内存可用时增加;如果内存有问题,则减少。DataSetLoader loader
与MultiDataSetLoader loader
- 当对一个“RDD<String>
”或“JavaRDD<String>
”进行评估时,这些都是可用的。它们是使用自定义用户定义函数将路径加载到DataSet或MultiDataSet的接口。大多数用户不需要使用这些功能,但是这个功能提供了更大的灵活性。例如,如果保存的小批处理文件格式不是DataSet/MultiDataSet,而是其他格式(可能是定制的),它们将被使用。
最后,如果希望保存(任何类型的)评估结果,可以将其保存为JSON格式,直接保存到远程存储,例如HDFS,如下所示:
JavaSparkContext sc = new JavaSparkContext();
Evaluation eval = ...
String json = eval.toJson();
String writeTo = "hdfs:///output/directory/evaluation.json";
SparkUtils.writeStringToFile(writeTo, json, sc); //Also supports local file paths - file://
用于SparkUtils的输入是
org.datavec.spark.transform.utils.SparkUtils
评估可以使用如下加载:
String json = SparkUtils.readStringFromFile(writeTo, sc);
Evaluation eval = Evaluation.fromJson(json);
如何保存(加载)Spark训练的神经网络
DL4J的Spark功能是围绕包装类的思想构建的,即,SparkDl4jMultiLayer和SparkComputationGraph内部使用标准的多层网络和计算图类。你可以分别使用SparkDl4jMultiLayer.getNetwork()和SparkComputationGraph.getNetwork()访问内部MultiLayerNetwork/ComputationGraph类。
为了在主机/驱动程序的本地文件系统上进行保存,请按照上述方式获得网络,并简单地使用ModelSerializer类或MultiLayerNetwork.save(File).load(File)
和ComputationGraph.save(File).load(File)
方法。
为了保存(或从远程位置或分布式文件系统(如HDFS)加载,可以使用输入和输出流。
例如,
JavaSparkContext sc = new JavaSparkContext();
FileSystem fileSystem = FileSystem.get(sc.hadoopConfiguration());
String outputPath = "hdfs:///my/output/location/file.bin";
MultiLayerNetwork net = sparkNet.getNetwork();
try (BufferedOutputStream os = new BufferedOutputStream(fileSystem.create(new Path(outputPath)))) {
ModelSerializer.writeModel(net, os, true);
}
读取是一个类似的过程:
JavaSparkContext sc = new JavaSparkContext();
FileSystem fileSystem = FileSystem.get(sc.hadoopConfiguration());
String outputPath = "hdfs:///my/output/location/file.bin";
MultiLayerNetwork net;
try(BufferedInputStream is = new BufferedInputStream(fileSystem.open(new Path(outputPath)))){
net = ModelSerializer.restoreMultiLayerNetwork(is);
}
如何执行分布式推理
DL4J的Spark实现支持分布式推理。也就是说,我们可以使用一个机器集群轻松地在输入RDD上生成预测。这种分布式推理还可以用于在单台机器上训练网络并被Spark加载(有关如何加载保存的网络以便与Spark一起使用的详细信息,请参阅保存/加载章节)。
注:如果要进行评估(即,计算精度、F1、MSE等),请参考评估操作指南。
用于执行分布式推理的方法签名如下:
SparkDl4jMultiLayer.feedForwardWithKey(JavaPairRDD<K, INDArray> featuresData, int batchSize) : JavaPairRDD<K, INDArray>
SparkComputationGraph.feedForwardWithKey(JavaPairRDD<K, INDArray[]> featuresData, int batchSize) : JavaPairRDD<K, INDArray[]>
当需要时,还存在接受输入掩码数组的重载。
注意,参数K-这是一个泛型类型,用于表示用于标识每个示例的唯一“键”。关值不被用作推理过程的一部分。这个键是必需的,因为Spark的RDD是无序的——如果没有这个键,我们就无法知道预测RDD中的哪个元素对应于输入RDD中的哪个元素。批量大小参数用于在执行推断时指定小批量大小。它不影响返回的值,而是用于平衡内存使用与计算效率:大批量计算总体上可能更快一些,但是需要更多的内存。在许多情况下,如果你不确定要使用什么,那么64的批量大小是尝试的好起点。
问题及故障排除指南
如何调试常见的Spark依赖问题(NoClassDefFoundExcption等)
Unfortunately, dependency problems at runtime can occur on a cluster if your project is not configured correctly. These problems can occur with any Spark jobs, not just those using DL4J - and they may be caused by other dependencies or libraries on the classpath, not by Deeplearning4j dependencies.
When dependency problems occur, they typically produce exceptions like:
不幸的是,如果项目配置不正确,则集群在运行时可能出现依赖性问题。这些问题可能出现在任何Spark作业中,而不仅仅是那些使用DL4J的作业,它们可能由类路径上的其他依赖项或库引起,而不是由DL4J依赖项引起。
当发生依赖性问题时,它们通常会产生如下异常:
- NoSuchMethodException
- ClassNotFoundException
- AbstractMethodError
例如,不匹配的Spark版本(试图在Spark 2集群上使用Spark 1)可以如下所示:
java.lang.AbstractMethodError: org.deeplearning4j.spark.api.worker.ExecuteWorkerPathMDSFlatMap.call(Ljava/lang/Object;)Ljava/util/Iterator;
另一类错误是UnsupportedClassVersionError
,例如java.lang.UnsupportedClassVersionError
错误:XYZ:Unsupported major.minor version 52.0
。这可能是由于试图在仅用Java 7 JRE/JDK建立的群集上运行(例如)Java 8代码。
如何调试依赖性问题:
步骤1:收集依赖信息
第一步(当使用Maven时)是生成可以引用的依赖树。打开命令行窗口(例如,Linux上的bash,Windows上的cmd),导航到Maven项目的根目录并运行 mvn dependency:tree
。这将为你提供一个依赖项列表(直接和瞬时的),该列表有助于准确理解类路径上的内容以及原因。
还请注意,mvn dependency:tree -Dverbose
将提供额外的信息,并且在调试与不匹配的库版本相关的问题时可能有用。
步骤2:检查你的Spark版本
当遇到依赖性问题时,请检查以下内容。
首先:检查Spark版本。如果你的集群正在运行Spark 2,你应该使用以_spark_2结尾的deeplearning4j-spark_2.10/2.11(以及DataVec)版本
仔细检查
如果发现问题,应该按照以下方式更改项目依赖性:在Spark 2(Scala 2.11)集群上,使用:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>dl4j-spark_2.11</artifactId>
<version>1.0.0-beta2_spark_2</version>
</dependency>
而在 Spark 1 (Scala 2.11) 集群,你应该使用:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>dl4j-spark_2.11</artifactId>
<version>1.0.0-beta2_spark_1</version>
</dependency>
步骤3: 检查 Scala 版本
Apache Spark发布了支持Scala 2.10和Scala 2.11的版本。
为了避免Scala版本的问题,你需要做两件事:(a)确保你的项目类路径上不存在Scala 2.10和Scala 2.11(或2.12)的混合依赖。检查依赖关系树中以_2.10或_2.11结尾的条目:例如,org.apache.spark:spark-core_2.11:jar:1.6.3:compile是Spark 1(1.6.3)依赖关系,使用Scala 2.11(b)确保项目与集群使用的内容匹配。例如,如果集群使用Scala 2.11运行Spark 2,那么所有的Scala依赖项都应该使用2.11。注意,Scala 2.11对于Spark集群更为常见。
如果发现不匹配的Scala版本,则需要通过更改pom.xml中的依赖关系版本(或其他依赖关系管理系统的类似配置文件)来对齐它们。许多库(包括Spark和DL4J)都发布了Scala 2.10和2.11版本的依赖项。
步骤4:检查不匹配的库版本
在Java生态系统中广泛使用的许多公用程序库在不同版本之间是不兼容的。例如,Spark可能依赖于库X版本Y,并且当库X版本Z在类路径上时将无法运行。此外,这些库中的许多被划分为多个模块(即,多个独立的模块依赖项),当混合不同版本时,这些模块不能正确工作。
一些常见的会导致问题的库包括:
- Jackson
- Guava
DL4J和ND4J使用这些库的版本,应该避免与Spark的依赖冲突。然而,其他的(第三方库)可能引入这些依赖项的版本。
通常,异常将给出在哪里查找的提示——即,堆栈跟踪可以包括特定的类,该类可用于识别有问题的库。
步骤5:一旦被标识,修复依赖冲突
要调试这类问题,请仔细检查依赖树( mvn dependency:tree -Dverbose
的输出)。必要时,可以使用排除项或添加有问题的依赖项作为直接依赖项,以便在问题中强制使用其版本。要做到这一点,你需要将你想要的版本的依赖项直接添加到项目中。通常情况下,这足以解决这个问题。
请记住,当使用Spark submit时,Spark将向驱动程序和主节点的类路径添加Spark及其依赖库的副本。这意味着对于Spark添加的依赖项,你不能简单地在项目中排除它们——Spark submit将在运行时添加它们,无论你是否在项目中排除它们。
另一个值得了解的额外设置是(实验)Spark配置选项,spark.driver.userClassPathFirst
和 spark.executor.userClassPathFirst
(有关详细信息,请参阅Spark配置文档)。在某些情况下,这些选项可能修复依赖性问题。
如何安全地缓存RDD[INDArray]和RDD[DataSet ]
Spark对于如何处理具有大的堆外组件的Java对象(例如DL4J中使用的DataSet和INDArray对象)存在一些问题。
要知道的要点是:
- MEMORY_ONLY 和 MEMORY_AND_DISK 由于Spark没有正确估计RDD中对象的大小,对于堆外内存,持久性可能会有问题。这可能导致内存不足(堆外)问题。
- 当持久化一个
RDD<DataSet>
或RDD<INDArray>
用于重用, 使用 MEMORY_ONLY_SER 或 MEMORY_AND_DISK_SER
为什么 MEMORY_ONLY_SER 或 MEMORY_AND_DISK_SER 被推荐
Apache Spark提高性能的方法之一是允许用户在内存中缓存数据。这可以通过使用RDD.cache()
或RDD.persist(StorageLevel.MEMORY_ONLY())
来将内容存储在内存中,以反序列化(即标准Java对象)的形式存储。基本思想很简单:如果你持久化一个RDD,则可以从内存(或磁盘,取决于配置)重新使用它,而不必重新计算它。然而,大的RDD可能不完全适合于内存。在这种情况下,RDD的一些部分必须重新计算或从磁盘加载,这取决于所使用的存储级别。此外,为了避免使用太多的内存,Spark会在需要时删除RDD的部分(块)。
下面列出了Spark中可用的主要存储级别。为了解释这些,请参Spark编程指南。
- MEMORY_ONLY
- MEMORY_AND_DISK
- MEMORY_ONLY_SER
- MEMORY_AND_DISK_SER
- DISK_ONLY
Spark的问题在于它是如何处理内存。特别地,Spark将根据估计的RDD(块)大小来删除RDD(块)的一部分。Spark估计块大小的方式取决于持久化级别。对于MEMORY_ONLY
和 MEMORY_AND_DISK持久化
,这是通过移动Java对象图来完成的,即,查看对象中的字段并递归地估计这些对象的大小。然而,这个过程并没有考虑DL4J或ND4J使用的堆外内存。对于像DataSets和INDArrays这样的对象(它们几乎全部存储在非堆中),Spark使用这个过程大大低估了对象的真实大小。此外,在决定是保留块还是删除块时,Spark只考虑堆内存的使用量。因为DataSet和INDArray对象在堆上的大小非常小,所以Spark将使用MEMORY_ONLY
和 MEMORY_AND_DISK
持久化保存太多对象,从而导致堆外内存耗尽,导致内存不足。
但是,对于MEMORY_ONLY_SER
和 MEMORY_AND_DISK_SER
,在Java堆上存储了序列化形式的块。Spark可以精确地估计以序列化形式存储的对象的大小(序列化对象没有堆外内存组件),因此Spark在需要时将丢弃块,从而避免任何内存不足的问题。
如何修复“Error querying NTP server”错误
DL4J的参数平均实现可以通过使用SparkDl4jMultiLayer.setCollectTrainingStats(true)收集训练统计数据。当启用此功能时,需要互联网访问来连接到NTP(网络时间协议)服务器。
有可能出现类似于NTPTimeSource错误:Error querying NTP server, attempt 1 of 10
. Sometimes these failures are transient (later retries will work) and can be ignored。然而,如果Spark集群被配置为使一个或多个工作节点不能访问因特网(或者具体地说,NTP服务器),则所有重试都可能失败。
有两种解决方案:
- 不使用
sparkNet.setCollectTrainingStats(true)
- 此功能是可选的(训练不需要),默认情况下禁用 - 将系统设置为使用本地机器时钟而不是NTP服务器作为时间源(但是要注意,结果时间线信息可能非常不准确)要使用系统时钟时间源,请在Spark submit中添加以下内容:
--conf spark.driver.extraJavaOptions=-Dorg.deeplearning4j.spark.time.TimeSource=org.deeplearning4j.spark.time.SystemClockTimeSource --conf spark.executor.extraJavaOptions=-Dorg.deeplearning4j.spark.time.TimeSource=org.deeplearning4j.spark.time.SystemClockTimeSource
Ubuntu 16.04的失败训练(Ubuntu bug可能影响DL4J Spark用户)
当在Ubuntu 16.04机器上的YARN集群上运行Spark时,很可能在完成作业后,运行Hadoop/YARN的用户拥有的所有进程都被杀死。这与Ubuntu中的一个bug有关,它被记录在https://bugs.launchpad.net/ubuntu/+source/procps/+bug/1610499。在Stackoverflow上也有关于此BUG的讨论http://stackoverflow.com/questions/38419078/logouts-while-running-hadoop-under-ubuntu-16-04。
提出了一些解决办法。
选项 1
添加
[login]
KillUserProcesses=no
到 /etc/systemd/logind.conf, 并重启。
选项 2
复制Ubuntu 14.04中的/bin /kill 二进制文件,并使用该二进制文件。
选项 3
降级到 Ubuntu 14.04
选项 4
在集群节点上运行 sudo loginctl enable-linger hadoop_user_name