web-dev-qa-db-fra.com

Spark performances pour Scala vs Python

Je préfère Python à Scala. Mais, comme Spark est nativement écrit en Scala, je m'attendais à ce que mon code soit exécuté plus rapidement dans la version Scala que la version Python pour des raisons évidentes.

Avec cette hypothèse, j'ai pensé apprendre et écrire la version Scala d'un code de prétraitement très courant pour environ 1 Go de données. Les données sont tirées du concours SpringLeaf sur Kaggle . Juste pour donner un aperçu des données (il contient 1936 dimensions et 145232 lignes). Les données sont composées de divers types, par ex. int, float, string, boolean. J'utilise 6 cœurs sur 8 pour le traitement de Spark; c'est pourquoi j'ai utilisé minPartitions=6 pour que chaque cœur puisse traiter quelque chose.

Code Scala

val input = sc.textFile("train.csv", minPartitions=6)

val input2 = input.mapPartitionsWithIndex { (idx, iter) => 
  if (idx == 0) iter.drop(1) else iter }
val delim1 = "\001"

def separateCols(line: String): Array[String] = {
  val line2 = line.replaceAll("true", "1")
  val line3 = line2.replaceAll("false", "0")
  val vals: Array[String] = line3.split(",")

  for((x,i) <- vals.view.zipWithIndex) {
    vals(i) = "VAR_%04d".format(i) + delim1 + x
  }
  vals
}

val input3 = input2.flatMap(separateCols)

def toKeyVal(line: String): (String, String) = {
  val vals = line.split(delim1)
  (vals(0), vals(1))
}

val input4 = input3.map(toKeyVal)

def valsConcat(val1: String, val2: String): String = {
  val1 + "," + val2
}

val input5 = input4.reduceByKey(valsConcat)

input5.saveAsTextFile("output")

Code Python

input = sc.textFile('train.csv', minPartitions=6)
DELIM_1 = '\001'


def drop_first_line(index, itr):
  if index == 0:
    return iter(list(itr)[1:])
  else:
    return itr

input2 = input.mapPartitionsWithIndex(drop_first_line)

def separate_cols(line):
  line = line.replace('true', '1').replace('false', '0')
  vals = line.split(',')
  vals2 = ['VAR_%04d%s%s' %(e, DELIM_1, val.strip('\"'))
           for e, val in enumerate(vals)]
  return vals2


input3 = input2.flatMap(separate_cols)

def to_key_val(kv):
  key, val = kv.split(DELIM_1)
  return (key, val)
input4 = input3.map(to_key_val)

def vals_concat(v1, v2):
  return v1 + ',' + v2

input5 = input4.reduceByKey(vals_concat)
input5.saveAsTextFile('output')

Scala Performance Stade 0 (38 min), Stade 1 (18 s) enter image description here

Performance Python Étape 0 (11 minutes), Étape 1 (7 s) enter image description here

Les deux produisent des graphes de visualisation DAG différents (en raison desquels les deux images montrent des fonctions différentes pour l'étape 0 pour Scala (map) et Python (reduceByKey))

Mais, essentiellement, les deux codes tentent de transformer les données en (dimension_id, chaîne de liste de valeurs) RDD et de les enregistrer sur le disque. La sortie servira à calculer diverses statistiques pour chaque dimension.

En ce qui concerne les performances, le code Scala pour ces données réelles semble fonctionner 4 fois plus lentement que le Python version. La bonne nouvelle pour moi est que cela m'a motivée à rester avec Python. La mauvaise nouvelle est que je n'ai pas bien compris pourquoi?

159
Mrityunjay

La réponse originale concernant le code se trouve ci-dessous.


Tout d'abord, vous devez faire la distinction entre différents types d'API, chacun ayant ses propres considérations de performances.

API RDD

(structures pures Python avec orchestration basée sur JVM)

C'est le composant qui sera le plus affecté par les performances du code Python et les détails de la mise en œuvre de PySpark. Bien que les performances de Python soient plutôt peu probables, vous devez tenir compte de quelques facteurs au moins:

  • Overhead de la communication JVM. Pratiquement toutes les données qui entrent et viennent de Python exécuteur doivent être transmises via un socket et un programme de travail JVM. Bien qu'il s'agisse d'une communication locale relativement efficace, elle n'est toujours pas gratuite.
  • Exécuteurs basés sur des processus (Python) par rapport à des exécutants (Scala) basés sur des threads (un seul thread multiple JVM). Chaque Python exécuteur s'exécute dans son propre processus. En tant qu'effet secondaire, il offre une isolation plus forte que celle de la version équivalente de la machine virtuelle Java et un certain contrôle sur le cycle de vie de l'exécuteur, mais une utilisation de la mémoire potentiellement considérablement plus importante:

    • empreinte mémoire de l'interprète
    • empreinte des bibliothèques chargées
    • diffusion moins efficace (chaque processus nécessite sa propre copie d'une émission)
  • Performances du code Python lui-même. D'une manière générale, Scala est plus rapide que Python, mais cela varie d'une tâche à l'autre. De plus, vous avez plusieurs options, y compris des JIT comme Numba , des extensions C ( Cython ) ou des bibliothèques spécialisées comme Theano . Finalement, si vous n'utilisez pas ML/MLlib (ou simplement la pile NumPy), envisagez d'utiliser PyPy comme interprète alternatif. Voir SPARK-3094 .

  • La configuration de PySpark fournit l'option spark.python.worker.reuse qui peut être utilisée pour choisir entre le processus de forçage Python pour chaque tâche et la réutilisation du processus existant. Cette dernière option semble être utile pour éviter une collecte des ordures coûteuse (il s'agit plus d'une impression que du résultat de tests systématiques), tandis que la première (par défaut) est optimale pour les émissions et les importations coûteuses.
  • Le comptage de références, utilisé comme méthode de récupération de place de première ligne dans CPython, fonctionne plutôt bien avec les charges de travail typiques Spark (traitement de type flux, sans cycles de référence) et réduit le risque de longues pauses GC.

MLlib

(exécution mixte Python et JVM)

Les considérations de base sont à peu près les mêmes qu'auparavant avec quelques problèmes supplémentaires. Alors que les structures de base utilisées avec MLlib sont des objets plain Python RDD, tous les algorithmes sont exécutés directement à l'aide de Scala.

Cela signifie un coût supplémentaire pour la conversion des objets Python en objets Scala et inversement, une utilisation accrue de la mémoire et certaines limitations supplémentaires que nous verrons plus tard.

À partir de maintenant (Spark 2.x), l'API basée sur RDD est en mode de maintenance et est sa suppression est prévue dans Spark 3. .

API DataFrame et Spark ML

(exécution JVM avec le code Python limité au pilote)

Ce sont probablement le meilleur choix pour les tâches de traitement de données standard. Étant donné que le code Python est généralement limité aux opérations logiques de haut niveau sur le pilote, il ne devrait y avoir aucune différence de performances entre Python et Scala.

Une seule exception est l’utilisation de fichiers UDF Python par ligne, nettement moins efficaces que leurs équivalents Scala. Bien que des améliorations soient possibles (il y a eu des développements substantiels dans Spark 2.0.0), la plus grande limitation concerne les allers-retours complets entre la représentation interne (JVM) et l'interprète Python. Si possible, vous devriez privilégier une composition d'expressions intégrées ( exemple . Python. Le comportement UDF a été amélioré dans Spark 2.0.0. encore sous-optimal par rapport à l'exécution native.Cela pourrait être amélioré à l'avenir avec l'introduction de DF vectorisées (SPARK-21190) .

Veillez également à éviter le transfert inutile de données entre DataFrames et RDDs. Cela nécessite une sérialisation et une désérialisation coûteuses, sans parler du transfert de données depuis et vers Python interprète.

Il est à noter que les appels Py4J ont une latence assez élevée. Cela comprend des appels simples tels que:

from pyspark.sql.functions import col

col("foo")

Généralement, cela ne devrait pas avoir d'importance (la surcharge est constante et ne dépend pas de la quantité de données), mais dans le cas d'applications logicielles en temps réel, vous pouvez envisager de mettre en cache/réutiliser les wrappers Java.

GraphX ​​et Spark DataSets

Pour l'instant (Spark 1,6 2.1) ni l'un ni l'autre ne fournit l'API PySpark afin que vous puissiez dire que PySpark est infiniment pire que Scala.

En pratique, le développement de GraphX ​​s’est presque complètement arrêté et le projet est actuellement en mode de maintenance avec les tickets JIRA associés fermés car ne règlent pas . La bibliothèque GraphFrames fournit une autre bibliothèque de traitement de graphe avec les liaisons Python.

Subjectivement, il n'y a pas beaucoup de place pour Datasets typé statiquement dans Python et même s'il existait la version actuelle de Scala, elle est trop simpliste et n'apporte pas les mêmes avantages en termes de performances. comme DataFrame.

Diffusion

D'après ce que j'ai vu jusqu'à présent, je vous recommande fortement d'utiliser Scala par rapport à Python. Cela pourrait changer à l'avenir si PySpark prend en charge les flux structurés, mais pour le moment Scala, l'API semble être beaucoup plus robuste, complète et efficace. Mon expérience est assez limitée.

La diffusion structurée dans Spark 2.x semble réduire l'écart entre les langues, mais pour l'instant, elle en est encore à ses débuts. Néanmoins, l'API basée sur RDD est déjà référencée en tant que "diffusion en continu" dans le documentation de Databricks (date d'accès 2017-03-03)), il est donc raisonnable de s'attendre à des efforts d'unification supplémentaires.

Considérations de non performance

Toutes les fonctionnalités Spark ne sont pas exposées via l'API PySpark. Assurez-vous de vérifier si les composants dont vous avez besoin sont déjà implémentés et essayez de comprendre les limitations possibles.

Cela est particulièrement important lorsque vous utilisez MLlib et des contextes mixtes similaires (voir Appel de la fonction Java/Scala à partir d'une tâche ). Pour être honnête, certaines parties de l'API PySpark, telles que mllib.linalg, fournissent un ensemble de méthodes plus complet que Scala.

L'API PySpark reflète fidèlement son équivalent Scala et, en tant que tel, n'est pas exactement Pythonic. Cela signifie qu'il est assez facile de mapper entre les langues, mais en même temps, le code Python peut être beaucoup plus difficile à comprendre.

Le flux de données PySpark est relativement complexe comparé à une exécution JVM pure. Il est beaucoup plus difficile de raisonner sur les programmes PySpark ou le débogage. De plus, au moins une compréhension de base de Scala et de la machine virtuelle Java est à peu près indispensable.

Passage continu vers l'API Dataset, avec l'API RDD gelée offre à la fois des opportunités et des défis pour les utilisateurs de Python. Bien que les composants de haut niveau de l'API soient beaucoup plus faciles à exposer en Python, les fonctionnalités les plus avancées sont quasiment impossibles à utiliser directement .

De plus, les fonctions natives Python continuent d'être des citoyens de seconde classe dans le monde SQL. Espérons que cela s’améliorera à l’avenir avec la sérialisation Apache Arrow ( données cibles des efforts actuels collection mais UDF serde est un objectif à long terme ).

Pour les projets qui dépendent fortement de la base de code Python, les alternatives pures Python (comme Dask ou Ray ) pourraient constituer une alternative intéressante.

Il ne doit pas être l'un contre l'autre

L'API Spark DataFrame (SQL, Dataset) offre un moyen élégant d'intégrer le code Scala/Java dans l'application PySpark. Vous pouvez utiliser DataFrames pour exposer des données à un code JVM natif et relire les résultats. J'ai expliqué certaines options ailleurs et vous pouvez trouver un exemple fonctionnel de voyage aller-retour Python-Scala dans Comment utiliser une classe Scala à l'intérieur de Pyspark .

Il peut être encore augmenté en introduisant des types définis par l'utilisateur (voir Comment définir un schéma pour un type personnalisé dans Spark SQL? ).


Quel est le problème avec le code fourni dans la question

(Avertissement: point de vue de Pythonista. J'ai probablement manqué quelques ruses Scala)

Tout d’abord, il y a une partie de votre code qui n’a aucun sens. Si vous avez déjà des paires (key, value) créées avec zipWithIndex ou enumerate, à quoi sert-il de créer une chaîne juste pour la scinder juste après? flatMap ne fonctionne pas de manière récursive, vous pouvez donc simplement donner des n-uplets et ignorer la suite de map.

Une autre partie que je trouve problématique est reduceByKey. De manière générale, reduceByKey est utile si l'application de la fonction d'agrégat peut réduire la quantité de données à mélanger. Puisque vous concaténez simplement des chaînes, il n’ya rien à gagner ici. En ignorant les éléments de bas niveau, tels que le nombre de références, la quantité de données que vous devez transférer est exactement la même que pour groupByKey.

Normalement, je ne m'attarderais pas là-dessus, mais pour autant que je sache, il s'agit d'un goulot d'étranglement dans votre code Scala. La jonction de chaînes sur la machine virtuelle Java est une opération relativement coûteuse (voir, par exemple: La concaténation de chaînes dans scala est-elle aussi coûteuse qu'en Java? ). Cela signifie que quelque chose comme ceci _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2) qui équivaut à input4.reduceByKey(valsConcat) dans votre code n'est pas une bonne idée.

Si vous voulez éviter groupByKey, vous pouvez essayer d'utiliser aggregateByKey avec StringBuilder. Quelque chose de semblable à cela devrait faire l'affaire:

rdd.aggregateByKey(new StringBuilder)(
  (acc, e) => {
    if(!acc.isEmpty) acc.append(",").append(e)
    else acc.append(e)
  },
  (acc1, acc2) => {
    if(acc1.isEmpty | acc2.isEmpty)  acc1.addString(acc2)
    else acc1.append(",").addString(acc2)
  }
)

mais je doute que cela en vaille la peine.

Gardant cela à l'esprit, j'ai réécrit votre code comme suit:

Scala :

val input = sc.textFile("train.csv", 6).mapPartitionsWithIndex{
  (idx, iter) => if (idx == 0) iter.drop(1) else iter
}

val pairs = input.flatMap(line => line.split(",").zipWithIndex.map{
  case ("true", i) => (i, "1")
  case ("false", i) => (i, "0")
  case p => p.swap
})

val result = pairs.groupByKey.map{
  case (k, vals) =>  {
    val valsString = vals.mkString(",")
    s"$k,$valsString"
  }
}

result.saveAsTextFile("scalaout")

Python :

def drop_first_line(index, itr):
    if index == 0:
        return iter(list(itr)[1:])
    else:
        return itr

def separate_cols(line):
    line = line.replace('true', '1').replace('false', '0')
    vals = line.split(',')
    for (i, x) in enumerate(vals):
        yield (i, x)

input = (sc
    .textFile('train.csv', minPartitions=6)
    .mapPartitionsWithIndex(drop_first_line))

pairs = input.flatMap(separate_cols)

result = (pairs
    .groupByKey()
    .map(lambda kv: "{0},{1}".format(kv[0], ",".join(kv[1]))))

result.saveAsTextFile("pythonout")

Résultats

En mode local[6] (processeur E3-1245 V2 @ 3.40GHz à Intel (R) Xeon (R)) avec 4 Go de mémoire par exécuteur, cela prend (n = 3):

  • Scala - moyenne: 250,00, stdev: 12,49
  • Python - moyenne: 246,66s, taille standard: 1,15

Je suis à peu près sûr que la majeure partie de ce temps est consacrée au remaniement, à la sérialisation, à la désérialisation et à d'autres tâches secondaires. Juste pour le plaisir, voici le code naif à un seul thread dans Python qui effectue la même tâche sur cette machine en moins d’une minute:

def go():
    with open("train.csv") as fr:
        lines = [
            line.replace('true', '1').replace('false', '0').split(",")
            for line in fr]
    return Zip(*lines[1:])
316
zero323

Extension aux réponses ci-dessus -

Scala s'avère plus rapide à bien des égards que python, mais il y a quelques raisons valables pour lesquelles python devient plus populaire que Scala.

Python pour Apache Spark est assez facile à apprendre et à utiliser. Cependant, ce n'est pas la seule raison pour laquelle Pyspark est un meilleur choix que Scala. Il y a plus.

L'API Python pour Spark peut être plus lente sur le cluster, mais à la fin, les scientifiques de données peuvent en faire beaucoup plus par rapport à Scala. La complexité de Scala est absente. L'interface est simple et complète.

Parler de la lisibilité du code, de la maintenance et de la familiarité avec Python API pour Apache Spark est bien meilleur que Scala.

Python est livré avec plusieurs bibliothèques liées à l’apprentissage automatique et au traitement du langage naturel. Cela facilite l’analyse des données et fournit également des statistiques bien matures et éprouvées. Par exemple, numpy, pandas, scikit-learn, seaborn et matplotlib.

Remarque: la plupart des scientifiques de données utilisent une approche hybride utilisant le meilleur des deux API.

Enfin, la communauté Scala s'avère souvent beaucoup moins utile pour les programmeurs. Cela fait de Python un apprentissage de grande valeur. Si vous connaissez suffisamment les langages de programmation statiques, tels que Java, vous pouvez cesser de vous inquiéter de ne pas utiliser Scala.

0
user11731048