Szukaj…


pmap

pmap przyjmuje funkcję (którą określasz) i stosuje ją do wszystkich elementów w tablicy. Ta praca jest podzielona między dostępnych pracowników. pmap następnie zwraca wyniki z tej funkcji do innej tablicy.

addprocs(3)
sqrts = pmap(sqrt, 1:10)

jeśli twoja funkcja pobiera wiele argumentów, możesz podać wiele wektorów do pmap

dots = pmap(dot, 1:10, 11:20)

Podobnie jak w przypadku @parallel , jeśli funkcja przekazana do pmap nie znajduje się w podstawowej Julii (tzn. Jest zdefiniowana przez użytkownika lub zdefiniowana w pakiecie), należy najpierw upewnić się, że funkcja jest dostępna dla wszystkich pracowników:

@everywhere begin
    function rand_det(n)
        det(rand(n,n))
    end
end

determinants = pmap(rand_det, 1:10)

Patrz również to SO Q i A.

@równolegle

@parallel może być użyty do sparaliżowania pętli, dzieląc kroki pętli w górę na różnych pracowników. Jako bardzo prosty przykład:

addprocs(3)

a = collect(1:10)

for idx = 1:10
    println(a[idx])
end

W przypadku nieco bardziej złożonego przykładu rozważ:

@time begin
    @sync begin
        @parallel for idx in 1:length(a)
            sleep(a[idx])
        end
    end
end
27.023411 seconds (13.48 k allocations: 762.532 KB)
julia> sum(a)
55

Widzimy zatem, że gdybyśmy wykonali tę pętlę bez @parallel , @parallel jej zajęłoby 55 sekund, a nie 27.

Możemy również dostarczyć operator redukcji dla makra @parallel . Załóżmy, że mamy tablicę, chcemy zsumować każdą kolumnę tablicy, a następnie pomnożyć te sumy przez siebie:

A = rand(100,100);

@parallel (*) for idx = 1:size(A,1)
    sum(A[:,idx])
end

Podczas korzystania z @parallel należy pamiętać o kilku ważnych sprawach, aby uniknąć nieoczekiwanego zachowania.

Po pierwsze: jeśli chcesz korzystać z funkcji w pętli, które nie są w podstawowej Julii (np. Funkcje zdefiniowane w skrypcie lub importowane z pakietów), musisz udostępnić te funkcje pracownikom. Dlatego na przykład następujące elementy nie działałyby:

myprint(x) = println(x)
for idx = 1:10
    myprint(a[idx])
end

Zamiast tego musielibyśmy użyć:

@everywhere begin
    function myprint(x) 
        println(x)
    end
end

@parallel for idx in 1:length(a)
    myprint(a[idx])
end

Po drugie Chociaż każdy pracownik będzie mógł uzyskać dostęp do obiektów w zakresie kontrolera, nie będzie mógł ich modyfikować. A zatem

a = collect(1:10)
@parallel for idx = 1:length(a)
   a[idx] += 1
end

julia> a'
1x10 Array{Int64,2}:
 1  2  3  4  5  6  7  8  9  10

Natomiast gdybyśmy wykonali pętlę bez @parallel, z powodzeniem zmodyfikowałaby tablicę a .

Aby rozwiązać ten możemy zamiast zrobić a do SharedArray typ obiektu tak, że każdy pracownik może dostęp i modyfikowanie go:

a = convert(SharedArray{Float64,1}, collect(1:10))
@parallel for idx = 1:length(a)
    a[idx] += 1
end

julia> a'
1x10 Array{Float64,2}:
 2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0  10.0  11.0

@spawn i @spawnat

Makra @spawn i @spawnat to dwa narzędzia, które Julia udostępnia do przypisywania zadań pracownikom. Oto przykład:

julia> @spawnat 2 println("hello world")
RemoteRef{Channel{Any}}(2,1,3)

julia>  From worker 2:  hello world

Oba te makra będą oceniać wyrażenie na procesie roboczym. Jedyna różnica między nimi polega na tym, że @spawnat pozwala wybrać, który pracownik oceni wyrażenie (w powyższym przykładzie podano pracownika 2), podczas gdy w @spawn pracownik zostanie automatycznie wybrany na podstawie dostępności.

W powyższym przykładzie po prostu mieliśmy pracownika 2 do wykonania funkcji println. Nie było nic interesującego do odzyskania lub odzyskania z tego. Często jednak wyrażenie, które wysłaliśmy do pracownika, przyniesie coś, co chcemy odzyskać. Zwróć uwagę w powyższym przykładzie, kiedy zadzwoniliśmy do @spawnat , zanim otrzymaliśmy wydruk z pracownika 2, zobaczyliśmy, co następuje:

RemoteRef{Channel{Any}}(2,1,3)

Oznacza to, że makro @spawnat zwróci obiekt typu RemoteRef . Ten obiekt z kolei będzie zawierał wartości zwracane z naszego wyrażenia, które jest wysyłane do pracownika. Jeśli chcemy pobrać te wartości, możemy najpierw przypisać RemoteRef który @spawnat zwraca do obiektu, a następnie użyć funkcji fetch() która działa na RemoteRef typu RemoteRef , aby pobrać wyniki zapisane z oceny wykonanej na pracownik.

julia> result = @spawnat 2 2 + 5
RemoteRef{Channel{Any}}(2,1,26)

julia> fetch(result)
7

Kluczem do skutecznego korzystania z @spawn jest zrozumienie natury wyrażeń , na których działa. Używanie @spawn do wysyłania poleceń do pracowników jest nieco bardziej skomplikowane niż tylko bezpośrednie wpisywanie tego, co byś @spawn gdybyś uruchomił „tłumacza” na jednym z pracowników lub natywnie wykonał na nich kod. Załóżmy na przykład, że chcemy użyć @spawnat do przypisania wartości zmiennej do pracownika. Możemy spróbować:

@spawnat 2 a = 5
RemoteRef{Channel{Any}}(2,1,2)

Zadziałało? Cóż, zobaczmy poprzez pracownika 2 próby drukowania . a

julia> @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,4)

julia> 

Nic się nie stało. Dlaczego? Możemy to zbadać bardziej szczegółowo, używając fetch() jak wyżej. fetch() może być bardzo przydatna, ponieważ pobiera nie tylko pomyślne wyniki, ale także komunikaty o błędach. Bez tego moglibyśmy nawet nie wiedzieć, że coś poszło nie tak.

julia> result = @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,5)

julia> fetch(result)
ERROR: On worker 2:
UndefVarError: a not defined

Komunikat o błędzie mówi, że a nie jest zdefiniowane dla pracownika 2. Ale dlaczego tak jest? Powodem jest to, że musimy zawinąć naszą operację przypisania do wyrażenia, które następnie używamy @spawn aby poinformować pracownika o ocenie. Poniżej znajduje się przykład z następującym wyjaśnieniem:

julia> @spawnat 2 eval(:(a = 2))
RemoteRef{Channel{Any}}(2,1,7)

julia> @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,8)

julia>  From worker 2:  2

Składnia :() jest tym, czego używa Julia do oznaczania wyrażeń . Następnie używamy funkcji eval() w Julii, która ocenia wyrażenie, i używamy makra @spawnat aby poinstruować, że wyrażenie należy ocenić na pracowniku 2.

Możemy również osiągnąć ten sam wynik, co:

julia> @spawnat(2, eval(parse("c = 5")))
RemoteRef{Channel{Any}}(2,1,9)

julia> @spawnat 2 println(c)
RemoteRef{Channel{Any}}(2,1,10)

julia>  From worker 2:  5

Ten przykład pokazuje dwa dodatkowe pojęcia. Po pierwsze, widzimy, że możemy również utworzyć wyrażenie za pomocą funkcji parse() wywołanej na łańcuchu. Po drugie, widzimy, że możemy używać nawiasów podczas wywoływania @spawnat , w sytuacjach, w których mogłoby to uczynić naszą składnię bardziej przejrzystą i łatwiejszą do zarządzania.

Kiedy używać @parallel vs. pmap

Julia dokumentacji informuje, że

pmap () jest przeznaczony dla przypadków, w których każde wywołanie funkcji wykonuje dużą ilość pracy. Natomiast @parallel for może obsłużyć sytuacje, w których każda iteracja jest niewielka, być może po prostu sumując dwie liczby.

Jest tego kilka przyczyn. Po pierwsze, pmap ponosi większe koszty początkowe, inicjując zlecenia dla pracowników. Tak więc, jeśli zadania są bardzo małe, te koszty początkowe mogą stać się nieefektywne. Z drugiej strony jednak pmap wykonuje „mądrzejszą” pracę polegającą na przydzielaniu miejsc pracy pracownikom. W szczególności buduje kolejkę zadań i wysyła nowe zadanie do każdego pracownika, ilekroć ten pracownik będzie dostępny. @parallel , dzieli wszystkie prace, które mają być wykonane między pracownikami, gdy są wywoływane. W związku z tym, jeśli niektórzy pracownicy podejmą pracę dłużej niż inni, możesz skończyć z sytuacją, w której większość pracowników zakończyła pracę i jest bezczynna, podczas gdy kilku pozostaje aktywnych przez nadmierną ilość czasu, kończąc pracę. Taka sytuacja jest jednak mniej prawdopodobna w przypadku bardzo małych i prostych prac.

Ilustruje to: załóżmy, że mamy dwóch pracowników, z których jeden jest wolny, a drugi dwa razy szybszy. Idealnie chcielibyśmy dać szybkiemu pracownikowi dwa razy więcej pracy niż pracownikowi powolnemu. (lub moglibyśmy mieć szybkie i wolne zadania, ale zlecenie jest dokładnie takie samo). pmap to osiągnie, ale @parallel nie.

Dla każdego testu inicjujemy:

addprocs(2)

@everywhere begin
    function parallel_func(idx)
        workernum = myid() - 1 
        sleep(workernum)
        println("job $idx")
    end
end

Teraz dla testu @parallel następujące czynności:

@parallel for idx = 1:12
    parallel_func(idx)
end

I odzyskaj wydruk:

julia>     From worker 2:    job 1
    From worker 3:    job 7
    From worker 2:    job 2
    From worker 2:    job 3
    From worker 3:    job 8
    From worker 2:    job 4
    From worker 2:    job 5
    From worker 3:    job 9
    From worker 2:    job 6
    From worker 3:    job 10
    From worker 3:    job 11
    From worker 3:    job 12

Jest prawie słodki. Pracownicy „dzielili się” pracą równomiernie. Zauważ, że każdy pracownik wykonał 6 zadań, mimo że pracownik 2 jest dwa razy szybszy niż pracownik 3. Może się dotykać, ale jest nieefektywny.

W przypadku testu pmap uruchamiam:

pmap(parallel_func, 1:12)

i uzyskaj wynik:

From worker 2:    job 1
From worker 3:    job 2
From worker 2:    job 3
From worker 2:    job 5
From worker 3:    job 4
From worker 2:    job 6
From worker 2:    job 8
From worker 3:    job 7
From worker 2:    job 9
From worker 2:    job 11
From worker 3:    job 10
From worker 2:    job 12

Teraz zauważ, że pracownik 2 wykonał 8 zadań, a pracownik 3 wykonał 4. Jest to dokładnie proporcjonalne do ich prędkości i tego, co chcemy dla optymalnej wydajności. pmap jest trudnym mistrzem zadań - od każdego według ich umiejętności.

@async i @sync

Zgodnie z dokumentacją pod ?@async , „ @async zawija wyrażenie w zadaniu”. Oznacza to, że dla wszystkiego, co mieści się w jego zakresie, Julia rozpocznie to zadanie, ale następnie przejdzie do tego, co będzie dalej w skrypcie, nie czekając na zakończenie zadania. Na przykład bez makra otrzymasz:

julia> @time sleep(2)
  2.005766 seconds (13 allocations: 624 bytes)

Ale dzięki makrze otrzymujesz:

julia> @time @async sleep(2)
  0.000021 seconds (7 allocations: 657 bytes)
Task (waiting) @0x0000000112a65ba0

julia> 

Julia pozwala zatem na kontynuowanie skryptu (i @time makra @time ) bez czekania na zakończenie zadania (w tym przypadku @time przez dwie sekundy).

Natomiast makro @sync@sync , aż wszystkie dynamicznie zamknięte zastosowania @async , @spawn , @spawnat i @parallel zostaną zakończone”. (zgodnie z dokumentacją pod ?@sync ). Widzimy zatem:

julia> @time @sync @async sleep(2)
  2.002899 seconds (47 allocations: 2.986 KB)
Task (done) @0x0000000112bd2e00

W tym prostym przykładzie nie ma więc sensu @async pojedynczej instancji @async i @sync razem. Ale, gdzie @sync może być przydatny, to gdzie @async zastosowano do wielu operacji, na które chcesz zezwolić, aby wszystkie rozpoczęły się jednocześnie, bez czekania na zakończenie każdej z nich.

Załóżmy na przykład, że mamy wielu pracowników i chcielibyśmy, aby każdy z nich pracował jednocześnie nad zadaniem, a następnie pobierał wyniki z tych zadań. Początkowa (ale niepoprawna) próba może być:

addprocs(2)
@time begin
    a = cell(nworkers())
    for (idx, pid) in enumerate(workers())
        a[idx] = remotecall_fetch(pid, sleep, 2)
    end
end
## 4.011576 seconds (177 allocations: 9.734 KB)

Problem polega na tym, że pętla czeka na remotecall_fetch() każdej remotecall_fetch() , tj. Na zakończenie każdego procesu (w tym przypadku remotecall_fetch() przez 2 sekundy) przed kontynuowaniem kolejnej operacji remotecall_fetch() . Jeśli chodzi o sytuację praktyczną, nie czerpiemy korzyści z równoległości, ponieważ nasze procesy nie wykonują swojej pracy (tj. Spania) jednocześnie.

Możemy to jednak poprawić, używając kombinacji makr @async i @sync :

@time begin
    a = cell(nworkers())
    @sync for (idx, pid) in enumerate(workers())
        @async a[idx] = remotecall_fetch(pid, sleep, 2)
    end
end
## 2.009416 seconds (274 allocations: 25.592 KB)

Jeśli teraz policzymy każdy krok pętli jako osobną operację, zobaczymy, że istnieją dwie osobne operacje poprzedzone @async . Makro pozwala na uruchomienie każdego z nich, a kod kontynuuje (w tym przypadku do następnego kroku pętli) przed każdym zakończeniem. Jednak użycie makra @sync , którego zasięg obejmuje całą pętlę, oznacza, że nie zezwalamy skryptowi na przejście poza tę pętlę, dopóki wszystkie operacje poprzedzone @async nie @async zakończone.

Możliwe jest jeszcze lepsze zrozumienie działania tych makr poprzez dalsze ulepszenie powyższego przykładu, aby zobaczyć, jak zmienia się on przy pewnych modyfikacjach. Załóżmy na przykład, że mamy @async bez @sync :

@time begin
    a = cell(nworkers())
    for (idx, pid) in enumerate(workers())
        println("sending work to $pid")
        @async a[idx] = remotecall_fetch(pid, sleep, 2)
    end
end
## 0.001429 seconds (27 allocations: 2.234 KB)

W tym przypadku makro @async pozwala nam kontynuować naszą pętlę, nawet zanim każda remotecall_fetch() zakończy wykonywanie. Ale, na lepsze lub gorsze, nie mamy makra @sync aby zapobiec dalszemu przechodzeniu kodu przez tę pętlę do momentu zakończenia wszystkich operacji remotecall_fetch() .

Niemniej jednak każda remotecall_fetch() jest nadal uruchomiona równolegle, nawet gdy będziemy kontynuować. Widzimy to, ponieważ jeśli poczekamy dwie sekundy, tablica a zawierająca wyniki będzie zawierać:

sleep(2)
julia> a
2-element Array{Any,1}:
 nothing
 nothing

(Element „none” jest wynikiem pomyślnego pobrania wyników funkcji uśpienia, które nie zwraca żadnych wartości)

Widzimy również, że dwie remotecall_fetch() zaczynają się zasadniczo w tym samym czasie, ponieważ poprzedzające je polecenia print również są wykonywane szybko po sobie (dane wyjściowe tych poleceń nie zostały pokazane tutaj). Porównaj to z kolejnym przykładem, w którym polecenia print wykonywane z 2-sekundowym opóźnieniem:

Jeśli @async makro @async w całej pętli (zamiast tylko jego wewnętrznego kroku), wtedy nasz skrypt będzie kontynuował natychmiast, nie czekając na zakończenie remotecall_fetch() . Teraz jednak zezwalamy tylko na to, aby skrypt kontynuował omijanie pętli jako całości. Nie zezwalamy na rozpoczęcie każdego kroku pętli przed zakończeniem poprzedniego. W związku z tym, w przeciwieństwie do powyższego przykładu, dwie sekundy po tym, jak skrypt kontynuuje działanie po pętli, tablica results nadal ma jeden element jako #undef co oznacza, że druga remotecall_fetch() nadal nie została zakończona.

@time begin
    a = cell(nworkers())
    @async for (idx, pid) in enumerate(workers())
        println("sending work to $pid")
        a[idx] = remotecall_fetch(pid, sleep, 2)
    end
end
# 0.001279 seconds (328 allocations: 21.354 KB)
# Task (waiting) @0x0000000115ec9120
## This also allows us to continue to

sleep(2)

a
2-element Array{Any,1}:
    nothing
 #undef    

I nie jest zaskoczeniem, że jeśli umieścimy @sync i @async obok siebie, otrzymamy, że każda remotecall_fetch() działa sekwencyjnie (a nie jednocześnie), ale nie kontynuujemy działania w kodzie, dopóki się nie skończy. Innymi słowy, byłby to w zasadzie odpowiednik, gdybyśmy nie mieli żadnego makra, podobnie jak sleep(2) zachowuje się zasadniczo identycznie jak @sync @async sleep(2)

@time begin
    a = cell(nworkers())
    @sync @async for (idx, pid) in enumerate(workers())
        a[idx] = remotecall_fetch(pid, sleep, 2)
    end
end
# 4.019500 seconds (4.20 k allocations: 216.964 KB)
# Task (done) @0x0000000115e52a10

Zauważ również, że możliwe jest wykonywanie bardziej skomplikowanych operacji w zakresie makra @async . Dokumentacja zawiera przykład zawierający całą pętlę w zakresie @async .

Przypomnij sobie, że pomoc dla makr synchronizacji mówi, że „Poczekaj, aż wszystkie dynamicznie zamknięte zastosowania @async , @spawn , @spawnat i @parallel zostaną zakończone”. Dla celów, które liczą się jako „ukończone”, ważne jest, w jaki sposób definiujesz zadania w ramach makr @sync i @async . Rozważ poniższy przykład, który jest niewielką odmianą jednego z przykładów podanych powyżej:

@time begin
    a = cell(nworkers())
    @sync for (idx, pid) in enumerate(workers())
        @async a[idx] = remotecall(pid, sleep, 2)
    end
end
## 0.172479 seconds (93.42 k allocations: 3.900 MB)

julia> a
2-element Array{Any,1}:
 RemoteRef{Channel{Any}}(2,1,3)
 RemoteRef{Channel{Any}}(3,1,4)

Wcześniejszy przykład trwał około 2 sekund, co wskazuje, że dwa zadania zostały uruchomione równolegle i że skrypt czeka, aż każde z nich zakończy wykonywanie swoich funkcji przed kontynuowaniem. Ten przykład ma jednak znacznie krótszą ocenę czasu. Powodem jest to, że dla celów @sync remotecall() „zakończyła się” po wysłaniu zadania do wykonania przez pracownika. (Zauważ, że wynikowa tablica, a tutaj, po prostu zawiera typy obiektów RemoteRef , co po prostu wskazuje, że dzieje się coś z określonym procesem, który teoretycznie może zostać pobrany w pewnym momencie w przyszłości). Natomiast remotecall_fetch() zakończyła się dopiero, gdy otrzyma wiadomość od pracownika, że jego zadanie zostało ukończone.

Dlatego jeśli szukasz sposobów, aby upewnić się, że pewne operacje z pracownikami zostały zakończone przed przejściem do skryptu (jak na przykład omówiono w tym poście ), należy dokładnie przemyśleć, co liczy się jako „zakończone” i jak to zrobisz zmierzyć, a następnie operacjonalizować to w skrypcie.

Dodawanie pracowników

Po pierwszym uruchomieniu Julii domyślnie będzie działał tylko jeden proces i będzie można go wykonać. Możesz to sprawdzić za pomocą:

julia> nprocs()
1

Aby skorzystać z przetwarzania równoległego, musisz najpierw dodać dodatkowych pracowników, którzy będą wtedy dostępni do wykonania powierzonej im pracy. Możesz to zrobić w skrypcie (lub z interpretera), używając: addprocs(n) gdzie n jest liczbą procesów, których chcesz użyć.

Alternatywnie możesz dodać procesy podczas uruchamiania Julii z wiersza poleceń, używając:

$ julia -p n

gdzie n to liczba dodatkowych procesów, które chcesz dodać. Zatem jeśli zaczniemy od Julii

$ julia -p 2

Kiedy Julia zacznie, otrzymamy:

julia> nprocs()
3


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow