Informatique distribuée dans Julia


Si l' article prĂ©cĂ©dent Ă©tait plus susceptible d'ĂȘtre prĂ©dĂ©fini, il est maintenant temps de tester les capacitĂ©s de parallĂ©lisation de Julia sur sa machine.


Traitement multicƓur ou distribuĂ©


L'implĂ©mentation du calcul parallĂšle avec mĂ©moire distribuĂ©e est assurĂ©e par le module Distributed dans le cadre de la bibliothĂšque standard fournie avec Julia. La plupart des ordinateurs modernes ont plus d'un processeur et plusieurs ordinateurs peuvent ĂȘtre regroupĂ©s. L'utilisation de la puissance de ces multiples processeurs vous permet d'effectuer de nombreux calculs plus rapidement. Les performances sont affectĂ©es par deux facteurs principaux: la vitesse des processeurs eux-mĂȘmes et la vitesse de leur accĂšs Ă  la mĂ©moire. Dans le cluster, il est Ă©vident que ce CPU aura l'accĂšs le plus rapide Ă  la RAM sur le mĂȘme ordinateur (nƓud). Peut-ĂȘtre encore plus surprenant, ces problĂšmes sont pertinents sur un ordinateur portable multicƓur typique en raison des diffĂ©rences de vitesse de la mĂ©moire principale et du cache. Par consĂ©quent, un bon environnement multiprocesseur devrait vous permettre de contrĂŽler la «propriĂ©té» d'une partie de la mĂ©moire par un processeur spĂ©cifique. Julia fournit un environnement multiprocesseur basĂ© sur la messagerie qui permet aux programmes de s'exĂ©cuter simultanĂ©ment sur plusieurs processus dans diffĂ©rents domaines de mĂ©moire.


L'implémentation de la messagerie de Julia est différente des autres environnements, tels que MPI [1] . La communication dans Julia est généralement «à sens unique», ce qui signifie que le programmeur doit contrÎler explicitement un seul processus dans une opération à deux processus. De plus, ces opérations ne ressemblent généralement pas à «l'envoi d'un message» et à la «réception d'un message», mais ressemblent plutÎt à des opérations d'un niveau supérieur, telles que les appels à des fonctions définies par l'utilisateur.


La programmation distribuĂ©e dans Julia repose sur deux primitives: les liaisons distantes et les appels distants . Un lien distant est un objet qui peut ĂȘtre utilisĂ© Ă  partir de n'importe quel processus pour faire rĂ©fĂ©rence Ă  un objet stockĂ© dans un processus particulier. Un appel distant est une demande d'un processus pour appeler une certaine fonction en fonction de certains arguments d'un autre (Ă©ventuellement le mĂȘme) processus.


Les liaisons distantes se présentent sous deux formes: Future et RemoteChannel .


Un appel distant renvoie Future et le fait immédiatement; le processus qui a effectué l'appel passe à sa prochaine opération, tandis que l'appel distant a lieu ailleurs. Vous pouvez attendre que l'appel distant se termine avec la commande wait pour le Future renvoyé, et vous pouvez également obtenir la valeur complÚte du résultat à l'aide de fetch .


D'un autre cĂŽtĂ©, nous avons des RemoteChannels qui sont réécrits. Par exemple, plusieurs processus peuvent coordonner leur traitement en se rĂ©fĂ©rant au mĂȘme canal distant. Chaque processus a un identifiant associĂ©. Le processus qui fournit l'invite interactive Julia a toujours un identificateur de 1. Les processus utilisĂ©s par dĂ©faut pour les opĂ©rations simultanĂ©es sont appelĂ©s «travailleurs». Lorsqu'il n'y a qu'un seul processus, le processus 1 est considĂ©rĂ© comme fonctionnant. Sinon, tous les processus autres que le processus 1 sont considĂ©rĂ©s comme des travailleurs.



Allons-y. julia -pn avec julia -pn postscript fournit n workflows sur l'ordinateur local. Il est gĂ©nĂ©ralement logique que n soit Ă©gal au nombre de threads CPU (cƓurs logiques) sur la machine. Notez que l'argument -p charge implicitement le module distribuĂ©.


Comment démarrer un postscript?

Les opĂ©rations de la console devraient ĂȘtre simples pour les utilisateurs de Linux, incl. Ce programme Ă©ducatif est destinĂ© aux utilisateurs inexpĂ©rimentĂ©s de Windows.
Terminal Julia (REPL) offre la possibilité d'utiliser des commandes systÚme:


 julia> pwd() #     "C:\\Users\\User\\AppData\\Local\\Julia-1.1.0" julia> cd("C:/Users/User/Desktop") #   julia> run(`calc`) #     #     Windows. #      Process(`calc`, ProcessExited(0)) 

en utilisant ces commandes, vous pouvez démarrer Julia à partir de Julia, mais il vaut mieux ne pas se laisser emporter



Il serait plus correct d'exĂ©cuter cmd depuis julia / bin / et d'y exĂ©cuter la commande julia -p 2 ou une option pour les amateurs de lancement Ă  partir d'un raccourci: sur le bureau, crĂ©ez un document de bloc-notes avec le contenu suivant C:\Users\User\AppData\Local\Julia-1.1.0\bin\julia -p 4 ( spĂ©cifiez l'adresse et le nombre de processus ) et enregistrez-le en tant que document texte sous le nom run.bat . Ici, maintenant sur votre bureau, il y a un fichier systĂšme de lancement Julia pour 4 cƓurs.


Vous pouvez utiliser une autre méthode (particuliÚrement pertinente pour Jupyter ):


 using Distributed addprocs(2) 

 $ ./julia -p 2 julia> r = remotecall(rand, 2, 2, 2) Future(2, 1, 4, nothing) julia> s = @spawnat 2 1 .+ fetch(r) Future(2, 1, 5, nothing) julia> fetch(s) 2×2 Array{Float64,2}: 1.18526 1.50912 1.16296 1.60607 

Le premier argument de remotecall est la fonction appelée.
La plupart des programmes simultanés dans Julia ne font pas référence à des processus spécifiques ou au nombre de processus disponibles, mais un appel distant est considéré comme une interface de bas niveau qui fournit un contrÎle plus précis.


Le deuxiÚme argument de remotecall est l'identifiant du processus qui fera le travail, et les arguments restants seront passés à la fonction appelée. Comme vous pouvez le voir, dans la premiÚre ligne, nous avons demandé au processus 2 de construire une matrice aléatoire 2 par 2, et dans la deuxiÚme ligne, nous avons demandé d'y ajouter 1. Le résultat des deux calculs est disponible en deux futures, r et s. La macro spawnat évalue l'expression dans le deuxiÚme argument au processus spécifié dans le premier argument. Parfois, vous pouvez avoir besoin d'une valeur calculée à distance. Cela se produit généralement lorsque vous lisez à partir d'un objet distant pour obtenir les données nécessaires à la prochaine opération locale. Il existe une fonction remotecall_fetch pour remotecall_fetch . C'est équivalent à fetch (remotecall (...)) , mais plus efficace.


Souvenez-vous que getindex(r, 1,1) équivalent à r[1,1] , donc cet appel récupÚre le premier élément du futur r .


La remotecall appel distant remotecall pas particuliĂšrement pratique. La macro @spawn facilite les @spawn . Il fonctionne avec une expression, pas une fonction, et choisit oĂč effectuer l'opĂ©ration pour vous:


 julia> r = @spawn rand(2,2) Future(2, 1, 4, nothing) julia> s = @spawn 1 .+ fetch(r) Future(3, 1, 5, nothing) julia> fetch(s) 2×2 Array{Float64,2}: 1.38854 1.9098 1.20939 1.57158 

Notez que nous avons utilisĂ© 1 .+ Fetch(r) au lieu de 1 .+ r C'est parce que nous ne savons pas oĂč le code sera exĂ©cutĂ©, donc dans le cas gĂ©nĂ©ral, il peut ĂȘtre nĂ©cessaire d'aller chercher pour dĂ©placer r dans le processus d'ajout. Dans ce cas, @spawn est suffisamment intelligent pour effectuer des calculs pour le processus propriĂ©taire de r , donc la rĂ©cupĂ©ration ne sera pas opĂ©rationnelle (aucun travail n'est effectuĂ©). (Il convient de noter que le spawn n'est pas intĂ©grĂ©, mais dĂ©fini dans Julia comme une macro. Vous pouvez dĂ©finir vos propres constructions de ce type.)


Il est important de se rappeler qu'aprÚs l'extraction, Future mettra en cache sa valeur localement. D'autres appels à récupérer n'entraßnent pas de saut de réseau. Une fois tous les contrats à terme référents sélectionnés, la valeur stockée supprimée est supprimée.


@async est similaire Ă  @spawn , mais exĂ©cute les tĂąches uniquement dans le processus local. Nous l'utilisons pour crĂ©er une tĂąche de «flux» pour chaque processus. Chaque tĂąche sĂ©lectionne l'index suivant, qui doit ĂȘtre calculĂ©, puis attend la fin du processus et se rĂ©pĂšte jusqu'Ă  Ă©puisement des index.


Notez que les tùches du chargeur ne démarrent pas avant que la tùche principale n'atteigne la fin du bloc @sync , aprÚs quoi elle passe le contrÎle et attend que toutes les tùches locales se terminent avant de revenir de la fonction.


Comme pour la version 0.7 et supĂ©rieure, les tĂąches du feeder peuvent partager l'Ă©tat via nextidx, car toutes sont exĂ©cutĂ©es dans le mĂȘme processus. MĂȘme si les tĂąches sont planifiĂ©es ensemble, le blocage peut ĂȘtre requis dans certains contextes, comme avec les E / S asynchrones. Cela signifie que le changement de contexte se produit uniquement Ă  des points bien dĂ©finis: dans ce cas, lorsque remotecall_fetch est remotecall_fetch . Il s'agit de l'Ă©tat actuel de l'implĂ©mentation, et il pourrait changer dans les futures versions de Julia, car il est conçu pour ĂȘtre en mesure d'exĂ©cuter jusqu'Ă  N tĂąches dans M processus, ou M: N Threading . Ensuite, nous avons besoin d'un modĂšle pour obtenir / libĂ©rer des verrous pour nextidx , car il n'est pas sĂ»r de permettre Ă  plusieurs processus de lire et d'Ă©crire des ressources en mĂȘme temps.


Votre code doit ĂȘtre disponible pour tout processus qui l'exĂ©cute. Par exemple, Ă  l'invite Julia, tapez ce qui suit:


 julia> function rand2(dims...) return 2*rand(dims...) end julia> rand2(2,2) 2×2 Array{Float64,2}: 0.153756 0.368514 1.15119 0.918912 julia> fetch(@spawn rand2(2,2)) ERROR: RemoteException(2, CapturedException(UndefVarError(Symbol("#rand2")) Stacktrace: [...] 

Le processus 1 connaissait la fonction rand2, mais pas le processus 2. Le plus souvent, vous téléchargerez du code à partir de fichiers ou de packages, et vous aurez une grande flexibilité pour contrÎler les processus qui chargent le code. Considérez le fichier DummyModule.jl contenant le code suivant:


 module DummyModule export MyType, f mutable struct MyType a::Int end f(x) = x^2+1 println("loaded") end 

Pour rĂ©fĂ©rencer MyType dans tous les processus, DummyModule.jl doit ĂȘtre chargĂ© dans chaque processus. Un appel Ă  include ('DummyModule.jl') charge pour un seul processus. Pour le charger dans chaque processus, utilisez la macro @everywhere (exĂ©cutez Julia avec julia -p 2):


 julia> @everywhere include("DummyModule.jl") loaded From worker 3: loaded From worker 2: loaded 

Comme d'habitude, cela ne rend pas DummyModule accessible à tout processus nécessitant une utilisation ou une importation. De plus, lorsqu'un module factice est inclus dans la portée d'un processus, il n'est inclus dans aucun autre:


 julia> using .DummyModule julia> MyType(7) MyType(7) julia> fetch(@spawnat 2 MyType(7)) ERROR: On worker 2: UndefVarError: MyType not defined ⋼ julia> fetch(@spawnat 2 DummyModule.MyType(7)) MyType(7) 

Cependant, il est toujours possible, par exemple, d'envoyer MyType au processus qui a chargĂ© le DummyModule, mĂȘme s'il n'est pas dans la portĂ©e:


 julia> put!(RemoteChannel(2), MyType(7)) RemoteChannel{Channel{Any}}(2, 1, 13) 

Le fichier peut Ă©galement ĂȘtre prĂ©chargĂ© dans plusieurs processus au dĂ©marrage avec l'indicateur -L, et le script du pilote peut ĂȘtre utilisĂ© pour contrĂŽler les calculs:


 julia -p <n> -L file1.jl -L file2.jl driver.jl 

Le processus Julia qui exécute le script de pilote dans l'exemple ci-dessus a un identificateur de 1, tout comme le processus qui fournit l'invite interactive. Enfin, si DummyModule.jl n'est pas un fichier séparé, mais un package, l'utilisation de DummyModule chargera DummyModule.jl dans tous les processus, mais le transférera uniquement dans la portée du processus pour lequel l'utilisation a été appelée.


Lancement et gestion des workflows


L'installation de base de Julia prend en charge deux types de clusters:


  • Le cluster local spĂ©cifiĂ© avec l'option -p, comme indiquĂ© ci-dessus.
  • Cluster des machines en utilisant l'option --machine-file. Celui-ci utilise la connexion ssh sans mot de passe pour dĂ©marrer les workflows Julia (sur le mĂȘme chemin que l'hĂŽte actuel) sur les machines spĂ©cifiĂ©es.


Les fonctions addprocs , rmprocs , worker et autres sont disponibles en tant qu'outil logiciel pour ajouter, supprimer et interroger des processus dans un cluster.


 julia> using Distributed julia> addprocs(2) 2-element Array{Int64,1}: 2 3 

Le module Distributed doit ĂȘtre explicitement chargĂ© dans le processus principal avant d'appeler addprocs . Il devient automatiquement disponible pour les workflows. Notez que les travailleurs ~/.julia/config/startup.jl pas le script de dĂ©marrage ~/.julia/config/startup.jl et ne synchronisent pas leur Ă©tat global (tels que les variables globales, les dĂ©finitions de nouvelles mĂ©thodes et les modules chargĂ©s) avec les autres processus en cours d'exĂ©cution. D'autres types de clusters peuvent ĂȘtre pris en charge en Ă©crivant votre propre ClusterManager , comme dĂ©crit ci-dessous dans la section ClusterManager .


Actions de données


L'envoi de messages et le déplacement de données constituent l'essentiel des frais généraux d'un programme distribué. La réduction du nombre de messages et de la quantité de données envoyées est essentielle pour atteindre les performances et l'évolutivité. Pour cela, il est important de comprendre le mouvement de données effectué par les différentes constructions de programmation distribuée de Julia.


fetch peut ĂȘtre considĂ©rĂ© comme une opĂ©ration de dĂ©placement de donnĂ©es explicite, car il demande directement le dĂ©placement d'un objet vers la machine locale. @spawn (et plusieurs constructions associĂ©es) dĂ©place Ă©galement les donnĂ©es, mais ce n'est pas si Ă©vident, donc on peut l'appeler une opĂ©ration de dĂ©placement de donnĂ©es implicite. ConsidĂ©rez ces deux approches pour construire et mettre au carrĂ© une matrice alĂ©atoire:


Temps de trajet:


 julia> A = rand(1000,1000); julia> Bref = @spawn A^2; [...] julia> fetch(Bref); 

deuxiÚme méthode:


 julia> Bref = @spawn rand(1000,1000)^2; [...] julia> fetch(Bref); 

La diffĂ©rence semble insignifiante, mais en fait elle est assez importante en raison du comportement de @spawn . Dans la premiĂšre mĂ©thode, une matrice alĂ©atoire est construite localement, puis envoyĂ©e Ă  un autre processus, oĂč elle est au carrĂ©. Dans la deuxiĂšme mĂ©thode, une matrice alĂ©atoire est construite et mise au carrĂ© sur un autre processus. Par consĂ©quent, la deuxiĂšme mĂ©thode envoie beaucoup moins de donnĂ©es que la premiĂšre. Dans cet exemple de jouet, les deux mĂ©thodes sont faciles Ă  distinguer et Ă  choisir. Cependant, dans un vrai programme, la conception d'un mouvement de donnĂ©es peut ĂȘtre trĂšs coĂ»teuse et probablement une certaine mesure.


Par exemple, si le premier processus a besoin de la matrice A, alors la premiĂšre mĂ©thode pourrait ĂȘtre meilleure. Ou, si le calcul de A est coĂ»teux et utilise uniquement le processus actuel, le dĂ©placer vers un autre processus peut ĂȘtre inĂ©vitable. Ou, si le processus actuel a trĂšs peu en commun entre le spawn et le fetch(Bref) , il pourrait ĂȘtre prĂ©fĂ©rable d'Ă©liminer complĂštement la concurrence. Ou imaginez que le rand(1000, 1000) remplacĂ© par une opĂ©ration plus coĂ»teuse. Ensuite, il peut ĂȘtre judicieux d'ajouter une autre instruction de spawn juste pour cette Ă©tape.


Variables globales


Les expressions exécutées à distance via le spawn, ou les fermetures spécifiées pour une exécution à distance à l'aide de remotecall , peuvent faire référence à des variables globales. Les liaisons globales du module Main sont gérées un peu différemment des liaisons globales des autres modules. Considérez l'extrait de code suivant:


 A = rand(10,10) remotecall_fetch(()->sum(A), 2) 

Dans ce cas, la sum DOIT ĂȘtre dĂ©finie dans le processus distant. Notez que A est une variable globale dĂ©finie dans l'espace de travail local. Le travailleur 2 n'a pas de variable nommĂ©e A dans la section Main . L'envoi de la fonction de fermeture () -> sum(A) pour le travailleur 2 entraĂźne la Main.A sur 2. Main.A continue d'exister sur le travailleur 2 mĂȘme aprĂšs avoir renvoyĂ© l'appel remotecall_fetch .



Les appels distants avec des références globales intégrées (dans le module principal uniquement) gÚrent les variables globales comme suit:


  • De nouvelles liaisons globales sont créées sur les postes de travail de destination si elles sont rĂ©fĂ©rencĂ©es dans le cadre d'un appel distant.
  • Les constantes globales sont Ă©galement dĂ©clarĂ©es comme constantes sur les nƓuds distants.
  • Les globaux ne sont soumis Ă  nouveau Ă  l'employĂ© cible que dans le contexte d'un appel distant et uniquement si sa valeur a changĂ©. En outre, le cluster ne synchronise pas les liaisons globales entre les nƓuds. Par exemple:

 A = rand(10,10) remotecall_fetch(()->sum(A), 2) # worker 2 A = rand(10,10) remotecall_fetch(()->sum(A), 3) # worker 3 A = nothing 

L'exĂ©cution du fragment ci-dessus conduit au fait que Main.A sur l'employĂ© 2 a une valeur diffĂ©rente de Main.A sur l'employĂ© 3, tandis que la valeur de Main.A sur le nƓud 1 est nulle.


Comme vous l'avez probablement compris, bien que la mĂ©moire associĂ©e aux variables globales puisse ĂȘtre collectĂ©e lorsqu'elles sont rĂ©affectĂ©es au pĂ©riphĂ©rique maĂźtre, de telles actions ne sont pas prises pour les travailleurs, car les liaisons continuent de fonctionner. clair! peut ĂȘtre utilisĂ© pour rĂ©affecter manuellement certaines variables globales Ă  nothing si elles ne sont plus nĂ©cessaires. Cela libĂ©rera toute la mĂ©moire qui leur est associĂ©e dans le cadre du cycle normal de collecte des ordures. Par consĂ©quent, les programmes doivent ĂȘtre prudents lors de l'accĂšs aux variables globales dans les appels distants. En fait, dans la mesure du possible, il vaut mieux les Ă©viter du tout. Si vous devez rĂ©fĂ©rencer des variables globales, envisagez d'utiliser des blocs let pour localiser des variables globales. Par exemple:


 julia> A = rand(10,10); julia> remotecall_fetch(()->A, 2); julia> B = rand(10,10); julia> let B = B remotecall_fetch(()->B, 2) end; julia> @fetchfrom 2 InteractiveUtils.varinfo() name size summary ––––––––– ––––––––– –––––––––––––––––––––– A 800 bytes 10×10 Array{Float64,2} Base Module Core Module Main Module 

Il est facile de voir que la variable globale A définie sur le travailleur 2, mais B écrite en tant que variable locale, et donc la liaison pour B n'existe pas sur le travailleur 2.


Boucles parallĂšles



Heureusement, de nombreux calculs de concurrence utiles ne nĂ©cessitent pas de mouvement de donnĂ©es. Un exemple typique est une simulation Monte Carlo, oĂč plusieurs processus peuvent simultanĂ©ment traiter des tests de simulation indĂ©pendants. Nous pouvons utiliser @spawn pour retourner des piĂšces en deux processus. Écrivez d'abord la fonction suivante dans count_heads.jl :


 function count_heads(n) c::Int = 0 for i = 1:n c += rand(Bool) end c end 

La fonction count_heads additionne simplement n bits aléatoires. Voici comment nous pouvons faire quelques tests sur deux machines et additionner les résultats:


 julia> @everywhere include_string(Main, $(read("count_heads.jl", String)), "count_heads.jl") julia> a = @spawn count_heads(100000000) Future(2, 1, 6, nothing) julia> b = @spawn count_heads(100000000) Future(3, 1, 7, nothing) julia> fetch(a)+fetch(b) 100001564 

Cet exemple illustre un modĂšle de programmation parallĂšle puissant et frĂ©quemment utilisĂ©. De nombreuses itĂ©rations sont effectuĂ©es indĂ©pendamment dans plusieurs processus, puis leurs rĂ©sultats sont combinĂ©s Ă  l'aide d'une fonction. Le processus d'union est appelĂ© rĂ©duction, car il rĂ©duit gĂ©nĂ©ralement le rang du tenseur: le vecteur de nombres est rĂ©duit Ă  un nombre, ou la matrice est rĂ©duite Ă  une ligne ou une colonne, etc. Dans le code, cela ressemble gĂ©nĂ©ralement Ă  ceci: motif x = f(x, v [i]) , oĂč x est la batterie, f est la fonction de rĂ©duction et v[i] est les Ă©lĂ©ments Ă  rĂ©duire.


Il est souhaitable que f soit associatif pour qu'il importe peu dans quel ordre les opĂ©rations sont effectuĂ©es. Veuillez noter que notre utilisation de ce modĂšle avec count_heads peut ĂȘtre gĂ©nĂ©ralisĂ©e. Nous avons utilisĂ© deux dĂ©clarations d' spawn explicites, ce qui limite la concurrence Ă  deux processus. Pour fonctionner sur un nombre quelconque de processus, nous pouvons utiliser un parallĂšle for boucle fonctionnant en mĂ©moire distribuĂ©e, qui peut ĂȘtre Ă©crit dans Julia en utilisant distribuĂ© , par exemple:


 nheads = @distributed (+) for i = 1:200000000 Int(rand(Bool)) end 

( (+) ). . .


, for , . , , , . , , . , :


 a = zeros(100000) @distributed for i = 1:100000 a[i] = i end 

, . , . , Shared Arrays , , :


 using SharedArrays a = SharedArray{Float64}(10) @distributed for i = 1:10 a[i] = i end 

«» , :


 a = randn(1000) @distributed (+) for i = 1:100000 f(a[rand(1:end)]) end 

f , . , , . , Future , . Future , fetch , , @sync , @sync distributed for .


, (, , ). , , Julia pmap . , :


 julia> M = Matrix{Float64}[rand(1000,1000) for i = 1:10]; julia> pmap(svdvals, M); 

pmap , . , distributed for , , , . pmap , distributed . distributed for .


(Shared Arrays)



Shared Arrays . DArray , SharedArray . DArray , ; , SharedArray .


SharedArray — , , . Shared Array SharedArrays , . SharedArray ( ) , , . SharedArray , . , Array , SharedArray , sdata . AbstractArray sdata , sdata Array . :


 SharedArray{T,N}(dims::NTuple; init=false, pids=Int[]) 

N - T dims , pids . , , pids ( , ).


init initfn(S :: SharedArray) , . , init , .


:


 julia> using Distributed julia> addprocs(3) 3-element Array{Int64,1}: 2 3 4 julia> @everywhere using SharedArrays julia> S = SharedArray{Int,2}((3,4), init = S -> S[localindices(S)] = myid()) 3×4 SharedArray{Int64,2}: 2 2 3 4 2 3 3 4 2 3 4 4 julia> S[3,2] = 7 7 julia> S 3×4 SharedArray{Int64,2}: 2 2 3 4 2 3 3 4 2 7 4 4 

SharedArrays.localindices . , , :


 julia> S = SharedArray{Int,2}((3,4), init = S -> S[indexpids(S):length(procs(S)):length(S)] = myid()) 3×4 SharedArray{Int64,2}: 2 2 2 2 3 3 3 3 4 4 4 4 

, , . Par exemple:


 @sync begin for p in procs(S) @async begin remotecall_wait(fill!, p, S, p) end end end 

. pid , , ( S ), pid .


«»:


 q[i,j,t+1] = q[i,j,t] + u[i,j,t] 

, , , , , : q [i,j,t] , q[i,j,t+1] , , , q[i,j,t] , q[i,j,t+1] . . . , (irange, jrange) , :


 julia> @everywhere function myrange(q::SharedArray) idx = indexpids(q) if idx == 0 # This worker is not assigned a piece return 1:0, 1:0 end nchunks = length(procs(q)) splits = [round(Int, s) for s in range(0, stop=size(q,2), length=nchunks+1)] 1:size(q,1), splits[idx]+1:splits[idx+1] end 

:


 julia> @everywhere function advection_chunk!(q, u, irange, jrange, trange) @show (irange, jrange, trange) # display so we can see what's happening for t in trange, j in jrange, i in irange q[i,j,t+1] = q[i,j,t] + u[i,j,t] end q end 

SharedArray


 julia> @everywhere advection_shared_chunk!(q, u) = advection_chunk!(q, u, myrange(q)..., 1:size(q,3)-1) 

, :


 julia> advection_serial!(q, u) = advection_chunk!(q, u, 1:size(q,1), 1:size(q,2), 1:size(q,3)-1); 

@distributed :


 julia> function advection_parallel!(q, u) for t = 1:size(q,3)-1 @sync @distributed for j = 1:size(q,2) for i = 1:size(q,1) q[i,j,t+1]= q[i,j,t] + u[i,j,t] end end end q end; 

, :


 julia> function advection_shared!(q, u) @sync begin for p in procs(q) @async remotecall_wait(advection_shared_chunk!, p, q, u) end end q end; 

SharedArray , ( julia -p 4 ):


 julia> q = SharedArray{Float64,3}((500,500,500)); julia> u = SharedArray{Float64,3}((500,500,500)); 

JIT- @time :


 julia> @time advection_serial!(q, u); (irange,jrange,trange) = (1:500,1:500,1:499) 830.220 milliseconds (216 allocations: 13820 bytes) julia> @time advection_parallel!(q, u); 2.495 seconds (3999 k allocations: 289 MB, 2.09% gc time) julia> @time advection_shared!(q,u); From worker 2: (irange,jrange,trange) = (1:500,1:125,1:499) From worker 4: (irange,jrange,trange) = (1:500,251:375,1:499) From worker 3: (irange,jrange,trange) = (1:500,126:250,1:499) From worker 5: (irange,jrange,trange) = (1:500,376:500,1:499) 238.119 milliseconds (2264 allocations: 169 KB) 

advection_shared! , , .



, . , , .
, , , .


- Julia


, , , .


π=3.14159265... , , -.  pi , S=πr2 oĂč r — . -  pi , .. [−1,1]2 x−y , .



( S=π , r=1 ) ( A=4 ) π/4 , , , . ,  pi , . compute_pi (N) ,  pi , N .


 function compute_pi(N::Int) # counts number of points that have radial coordinate < 1, ie in circle n_landed_in_circle = 0 for i = 1:N x = rand() * 2 - 1 # uniformly distributed number on x-axis y = rand() * 2 - 1 # uniformly distributed number on y-axis r2 = x*x + y*y # radius squared, in radial coordinates if r2 < 1.0 n_landed_in_circle += 1 end end return n_landed_in_circle / N * 4.0 end 

, ,  pi . : , , 25 .


Julia Pi.jl ( Sublime Text , ):


 C:\Users\User\AppData\Local\Julia-1.1.0\bin\julia -p 4 julia> include("C:/Users/User/Desktop/Pi.jl") 


 using Distributed addprocs(4) 

Jupyter


Pi.jl
 @everywhere function compute_pi(N::Int) n_landed_in_circle = 0 # counts number of points that have radial coordinate < 1, ie in circle for i = 1:N x = rand() * 2 - 1 # uniformly distributed number on x-axis y = rand() * 2 - 1 # uniformly distributed number on y-axis r2 = x*x + y*y # radius squared, in radial coordinates if r2 < 1.0 n_landed_in_circle += 1 end end return n_landed_in_circle / N * 4.0 end function parallel_pi_computation(N::Int; ncores::Int=4) #       sum_of_pis = @distributed (+) for i=1:ncores compute_pi(ceil(Int, N / ncores)) end return sum_of_pis / ncores # average value end # ceil (T, x)     #  T,     x. 

, :


 julia> @time parallel_pi_computation(1000000000, ncores = 1) 6.818123 seconds (1.96 M allocations: 99.838 MiB, 0.42% gc time) 3.141562892 julia> @time parallel_pi_computation(1000000000, ncores = 1) 5.081638 seconds (1.12 k allocations: 62.953 KiB) 3.141657252 julia> @time parallel_pi_computation(1000000000, ncores = 2) 3.504871 seconds (1.84 k allocations: 109.382 KiB) 3.1415942599999997 julia> @time parallel_pi_computation(1000000000, ncores = 4) 3.093918 seconds (1.12 k allocations: 71.938 KiB) 3.1416889400000003 julia> pi ? = 3.1415926535897... 

JIT - — . , Julia . , ( Multi-Threading, Atomic Operations, Channels Coroutines).


Liens utiles


, , . MPI.jl MPI ,
DistributedArrays.jl .
GPU, :


  1. ( C) OpenCL.jl CUDAdrv.jl OpenCL CUDA.
  2. ( Julia) CUDAnative.jl CUDA .
  3. , , CuArrays.jl CLArrays.jl
  4. , ArrayFire.jl GPUArrays.jl
  5. -
  6. Kynseed

, , . !


Source: https://habr.com/ru/post/fr455846/


All Articles