Медленная производительность с использованием Apache Spark Gradient Boosted Tree

Я экспериментирую с Gradient Boosted Trees алгоритмом обучения из библиотеки ML от Spark 1.4. Я решаю проблему с двоичной классификацией, где мой вход составляет ~ 50 000 выборок и ~ 500 000 функций. Моя цель - вывести определение результирующего ансамбля GBT в формате для чтения человеком. Мой опыт до сих пор заключается в том, что для моего размера проблемы добавление большего количества ресурсов в кластер, похоже, не влияет на продолжительность прогона. Кажется, что 10-итерационный тренировочный пробег занимает 13 часов. Это неприемлемо, так как я пытаюсь выполнить 100-300 итераций, и время выполнения, похоже, взорвется с количеством итераций.

Приложение My Spark

Это не точный код, но его можно свести к:

SparkConf sc = new SparkConf().setAppName("GBT Trainer")
            // unlimited max result size for intermediate Map-Reduce ops.
            // Having no limit is probably bad, but I've not had time to find
            // a tighter upper bound and the default value wasn't sufficient.
            .set("spark.driver.maxResultSize", "0");
JavaSparkContext jsc = new JavaSparkContext(sc)

// The input file is encoded in plain-text LIBSVM format ~59GB in size
<LabeledPoint> data = MLUtils.loadLibSVMFile(jsc.sc(), "s3://somebucket/somekey/plaintext_libsvm_file").toJavaRDD();

BoostingStrategy boostingStrategy = BoostingStrategy.defaultParams("Classification");
boostingStrategy.setNumIterations(10);
boostingStrategy.getTreeStrategy().setNumClasses(2);
boostingStrategy.getTreeStrategy().setMaxDepth(1);
Map<Integer, Integer> categoricalFeaturesInfo = new HashMap<Integer, Integer>();
boostingStrategy.treeStrategy().setCategoricalFeaturesInfo(categoricalFeaturesInfo);

GradientBoostedTreesModel model = GradientBoostedTrees.train(data, boostingStrategy);

// Somewhat-convoluted code below reads in Parquete-formatted output
// of the GBT model and writes it back out as json.
// There might be cleaner ways of achieving the same, but since output
// size is only a few KB I feel little guilt leaving it as is.

// serialize and output the GBT classifier model the only way that the library allows
String outputPath = "s3://somebucket/somekeyprefex";
model.save(jsc.sc(), outputPath + "/parquet");
// read in the parquet-formatted classifier output as a generic DataFrame object
SQLContext sqlContext = new SQLContext(jsc);
DataFrame outputDataFrame = sqlContext.read().parquet(outputPath + "/parquet"));    
// output DataFrame-formatted classifier model as json           
outputDataFrame.write().format("json").save(outputPath + "/json");

Вопрос

Каково узкое место производительности моего приложения Spark (или самого алгоритма обучения GBT) на входе этого размера и как я могу добиться большего выполнения parallelism?

Я все еще новичок Spark dev, и я был бы признателен за любые советы по конфигурации кластера и профилированию выполнения.

Подробнее о настройке кластера

Я запускаю это приложение в кластере AWS EMR (emr-4.0.0, режим кластеров YARN) экземпляров r3.8xlarge (32 ядра, каждая по 244 ГБ). Я использую такие большие экземпляры, чтобы максимизировать гибкость распределения ресурсов. До сих пор я пытался использовать 1-3 r3.8x больших экземпляров с различными схемами распределения ресурсов между драйвером и рабочими. Например, для кластера из 1 r3.8x больших экземпляров я отправляю приложение следующим образом:

aws emr add-steps --cluster-id $1 --steps Name=$2,\
Jar=s3://us-east-1.elasticmapreduce/libs/script-runner/script-runner.jar,\
Args=[/usr/lib/spark/bin/spark-submit,--verbose,\
--deploy-mode,cluster,--master,yarn,\
--driver-memory,60G,\
--executor-memory,30G,\
--executor-cores,5,\
--num-executors,6,\
--class,GbtTrainer,\
"s3://somebucket/somekey/spark.jar"],\
ActionOnFailure=CONTINUE

Для кластера из 3-х кратных экземпляров я увеличиваю выделение ресурсов:

--driver-memory,80G,\
--executor-memory,35G,\
--executor-cores,5,\
--num-executors,18,\

У меня нет четкого представления о том, сколько памяти полезно придать каждому исполнителю, но я чувствую, что в любом случае я щедр. Просматривая интерфейс Spark, я не вижу задачи с размером ввода более нескольких ГБ. Я руляю на стороне осторожности, давая драйверу столько памяти, чтобы убедиться, что это не память, голодная для любых промежуточных операций агрегации результатов.

Я пытаюсь сохранить количество ядер на каждом исполнителе до 5 в соответствии с предложениями в Cloudera Как настроить серию Spark Jobs (по их мнению, больше что 5 ядер имеют тенденцию вводить узкое место HDFS IO). Я также убеждаюсь, что осталось достаточно запасных ОЗУ и процессоров для ОС хоста и Hadoop.

Мои результаты до сих пор

Моя единственная подсказка - это Spark UI, показывающий очень длинную Запланирующую задержку для ряда задач в конце выполнения. У меня также возникает ощущение, что временная шкала этапов/задач, отображаемая пользовательским интерфейсом Spark, не учитывает все время, которое требуется для завершения работы. Я подозреваю, что приложение драйвера застревает, выполняя какую-то длительную операцию либо в конце каждой итерации обучения, либо в конце всего тренировочного прогона.

Я уже провел справедливое исследование по настройке приложений Spark. В большинстве статей приводятся отличные рекомендации по использованию RDD-операций, которые уменьшают размер промежуточного ввода или избегают перетасовки данных между этапами. В моем случае я в основном использую алгоритм "из коробки", который написан экспертами ML и должен быть хорошо настроен в этом отношении. Мой собственный код, который выводит модель GBT на S3, должен занимать тривиальное количество времени для запуска.

Ответ 1

Я не использовал реализацию MLLibs GBT, но я использовал оба

LightGBM и XGBoost успешно. Я настоятельно рекомендую взглянуть на эти другие библиотеки.

В целом, реализации GBM должны обучать модели итеративно, поскольку они учитывают потерю всего ансамбля при построении следующего дерева. Это делает обучение GBM узким местом и не может быть легко распараллеливаемым (в отличие от случайных лесов, которые тривиально распараллеливаемы). Я ожидаю, что он будет работать лучше с меньшим количеством задач, но это может быть не вся ваша проблема. Поскольку у вас так много функций 500K, вы будете иметь очень большие накладные расходы при расчете гистограмм и точек разделения во время тренировки. Вам следует уменьшить количество имеющихся у вас функций, тем более что они намного больше, чем количество выборок, которые приведут к его переобучению.

Что касается настройки вашего кластера: Вы хотите минимизировать перемещение данных, поэтому меньше исполнителей с большим объемом памяти. 1 исполнитель на экземпляр ec2, с количеством ядер, установленным для того, что предоставляет экземпляр.

Ваши данные достаточно малы, чтобы поместиться в ~ 2 EC2 такого размера. Предполагая, что вы используете double (8 байт), получается 8 * 500000 * 50000 = 200 ГБ. Попробуйте загрузить все это в оперативную память, используя .cache() на вашем фрейме данных. Если вы выполняете операцию над всеми строками (например, суммой), вы должны принудительно загрузить ее, и вы можете измерить, сколько времени занимает ввод-вывод. Как только он в оперативной памяти и кэширует любые другие операции над ним будет быстрее.