web-dev-qa-db-fra.com

OpenMP Dynamic vs Guided Scheduling

J'étudie la planification d'OpenMP et en particulier les différents types. Je comprends le comportement général de chaque type, mais des éclaircissements seraient utiles sur le moment de choisir entre dynamic et guided planification.

documents d'Intel décrivent dynamic planification:

Utilisez la file d'attente de travail interne pour donner un bloc d'itérations de boucle de la taille d'un bloc à chaque thread. Lorsqu'un thread est terminé, il récupère le bloc d'itérations de boucle suivant en haut de la file d'attente de travail. Par défaut, la taille du segment est 1. Soyez prudent lorsque vous utilisez ce type de planification en raison de la surcharge supplémentaire impliquée.

Il décrit également guided la planification:

Similaire à l'ordonnancement dynamique, mais la taille des morceaux commence à grande et diminue pour mieux gérer le déséquilibre de charge entre les itérations. Le paramètre de bloc facultatif spécifie les blocs de taille minimale à utiliser. Par défaut, la taille du bloc est approximativement loop_count/number_of_threads.

Étant donné que guided l'ordonnancement diminue de façon dynamique la taille des blocs à l'exécution, pourquoi devrais-je jamais utiliser l'ordonnancement dynamic?

J'ai fait des recherches sur cette question et a trouvé ce tableau de Dartmouth :

enter image description here

guided est répertorié comme ayant high surcharge, tandis que dynamic a une surcharge moyenne.

Cela avait initialement un sens, mais après une enquête plus approfondie, j'ai lire un article Intel sur le sujet. D'après le tableau précédent, j'ai théorisé que l'ordonnancement de guided prendrait plus de temps en raison de l'analyse et des ajustements de la taille de bloc lors de l'exécution (même lorsqu'il est utilisé correctement). Cependant, dans l'article d'Intel, il indique:

Les horaires guidés fonctionnent mieux avec de petites tailles de bloc comme limite; cela donne le plus de flexibilité. On ne sait pas pourquoi elles s’aggravent à des tailles de bloc plus importantes, mais elles peuvent prendre trop de temps si elles sont limitées à de grandes tailles de bloc.

Pourquoi la taille du bloc serait-elle liée au fait que guided prend plus de temps que dynamic? Il serait logique que le manque de "flexibilité" entraîne une perte de performances en verrouillant la taille de bloc trop élevée. Cependant, je ne décrirais pas cela comme une "surcharge", et le problème de verrouillage discréditerait la théorie précédente.

Enfin, il est indiqué dans l'article:

Les plannings dynamiques offrent la plus grande flexibilité, mais prennent le plus grand impact sur les performances lorsqu'ils sont mal programmés.

Il est logique que l'ordonnancement dynamic soit plus optimal que static, mais pourquoi est-il plus optimal que guided? Est-ce juste les frais généraux que je remets en question?

Ceci quelque peu lié SO post explique NUMA lié aux types de planification. Cela n'a aucun rapport avec cette question, car l'organisation requise est perdue par le "premier arrivé, premier servi") comportement de ces types de planification.

dynamic la planification peut être coalescente, entraînant une amélioration des performances, mais la même hypothèse devrait s'appliquer à guided.

Voici le calendrier de chaque type de planification sur différentes tailles de blocs de l'article Intel pour référence. Ce ne sont que des enregistrements d'un programme et certaines règles s'appliquent différemment par programme et par machine (en particulier avec la programmation), mais cela devrait fournir les tendances générales.

enter image description here

[~ # ~] éditez [~ # ~] (cœur de ma question):

  • Qu'est-ce qui affecte le temps d'exécution de la planification de guided? Exemples spécifiques? Pourquoi est-il plus lent que dynamic dans certains cas?
  • Quand devrais-je privilégier guided plutôt que dynamic ou vice versa?
  • Une fois que cela a été expliqué, les sources ci-dessus soutiennent-elles votre explication? Se contredisent-ils du tout?
20
Matt Goodrich

Qu'est-ce qui affecte le temps d'exécution de la planification guidée?

Il y a trois effets à considérer:

1. Équilibre de charge

L'intérêt de l'ordonnancement dynamique/guidé est d'améliorer la distribution du travail dans le cas où chaque itération de boucle ne contient pas la même quantité de travail. Fondamentalement:

  • schedule(dynamic, 1) fournit un équilibrage de charge optimal
  • dynamic, k Aura toujours identique ou meilleur équilibrage de charge que guided, k

La norme stipule que la taille de chaque bloc est proportionnelle au nombre d'itérations non attribuées divisé par le nombre de threads dans l'équipe, diminuant à k.

Le exécution de GCC OpenMP prend cela littéralement, en ignorant le proportionnel. Par exemple, pour 4 threads, k=1, Il y aura 32 itérations comme 8, 6, 5, 4, 3, 2, 1, 1, 1, 1. Maintenant, à mon humble avis, c'est vraiment stupide: cela entraîne un mauvais déséquilibre de charge si les premières itérations 1/n contiennent plus de 1/n du travail.

Exemples spécifiques? Pourquoi est-il plus lent que dynamique dans certains cas?

Ok, jetons un coup d'œil à un exemple trivial où le travail intérieur diminue avec l'itération de la boucle:

#include <omp.h>

void work(long ww) {
    volatile long sum = 0;
    for (long w = 0; w < ww; w++) sum += w;
}

int main() {
    const long max = 32, factor = 10000000l;
    #pragma omp parallel for schedule(guided, 1)
    for (int i = 0; i < max; i++) {
         work((max - i) * factor);
    }
}

L'exécution ressemble à ceci1:

Example execution timeline

Comme vous pouvez le voir, guided fait vraiment mal ici. guided fera beaucoup mieux pour différents types de distributions de travail. Il est également possible de mettre en œuvre guidée différemment. L'implémentation en clang (dont l'IIRC provient d'Intel) est beaucoup plus sophistiquée . Je ne comprends vraiment pas l'idée derrière l'implémentation naïve de GCC. À mes yeux, cela défait efficacement le but de l'effacement dynamique de la charge si vous donnez 1/n Du travail au premier thread.

2. Frais généraux

Désormais, chaque segment dynamique a un impact sur les performances en raison de l'accès à l'état partagé. La surcharge de guided sera légèrement plus élevée par morceau que dynamic, car il y a un peu plus de calcul à faire. Cependant, guided, k Aura moins de blocs dynamiques totaux que dynamic, k.

Les frais généraux dépendront également de la mise en œuvre, par exemple qu'il utilise atomiques ou verrous pour protéger l'état partagé.

3. Effets cache et NUMA

Disons écrire sur un vecteur d'entiers dans l'itération de votre boucle. Si chaque seconde itération devait être exécutée par un thread différent, chaque deuxième élément du vecteur serait écrit par un noyau différent. C'est vraiment mauvais car ce faisant, ils concurrencent les lignes de cache qui contiennent des éléments voisins (faux partage). Si vous avez de petites tailles de morceaux et/ou des tailles de morceaux qui ne s'alignent pas bien sur les caches, vous obtenez de mauvaises performances aux "bords" des morceaux. C'est pourquoi vous préférez généralement de grandes tailles de morceaux Nice (2^n). guided peut vous donner des tailles de morceaux plus grandes en moyenne, mais pas 2^n (ou k*m).

Cette réponse (que vous avez déjà référencé), discute longuement de l'inconvénient de la planification dynamique/guidée en termes de NUMA, mais cela s'applique également aux localités/caches.

Ne devinez pas, mesurez

Étant donné les différents facteurs et la difficulté de prévoir des spécificités, je ne peux que recommander de mesurer votre application spécifique, sur votre système spécifique, dans votre configuration spécifique, avec votre compilateur spécifique. Malheureusement, il n'y a pas de portabilité parfaite des performances. Je dirais personnellement que c'est particulièrement vrai pour guided.

Quand est-ce que je préfèrerais être guidé plutôt que dynamique ou vice-versa?

Si vous avez des connaissances spécifiques sur la surcharge/travail par itération, je dirais que dynamic, k Vous donne les résultats les plus stables en choisissant un bon k. En particulier, vous ne dépendez pas tellement de la façon dont la mise en œuvre est intelligente.

D'un autre côté, guided peut être une bonne première supposition, avec un rapport frais généraux/équilibrage de charge, au moins pour une implémentation intelligente. Soyez particulièrement prudent avec guided si vous savez que les temps d'itération ultérieurs sont plus courts.

Gardez à l'esprit qu'il existe également schedule(auto), qui donne un contrôle complet sur le compilateur/runtime, et schedule(runtime), qui vous permet de sélectionner la politique de planification pendant l'exécution.

Une fois que cela a été expliqué, les sources ci-dessus soutiennent-elles votre explication? Se contredisent-ils du tout?

Prenez les sources, y compris cet anser, avec un grain de sel. Ni le graphique que vous avez publié, ni ma photo chronologique ne sont des chiffres scientifiquement exacts. Il y a une énorme variation dans les résultats et il n'y a pas de barres d'erreur, elles seraient probablement partout avec ces très peu de points de données. Le graphique combine également les multiples effets que j'ai mentionnés sans divulguer le code Work.

[De la documentation Intel]

Par défaut, la taille du bloc est approximativement loop_count/number_of_threads.

Cela contredit mon observation selon laquelle l'icc gère beaucoup mieux mon petit exemple.

1: Utilisation de GCC 6.3.1, Score-P/Vampir pour la visualisation, deux fonctions de travail alternées pour la coloration.

23
Zulan