Julia Language
병렬 처리
수색…
pmap
pmap
은 (당신이 지정한) 함수를 취해 배열의 모든 요소에 적용합니다. 이 작업은 사용 가능한 작업자들로 나뉘어져 있습니다. 그런 다음 pmap
은 해당 함수의 결과를 다른 배열에 반환합니다.
addprocs(3)
sqrts = pmap(sqrt, 1:10)
함수가 여러 인수를 취하는 경우 여러 벡터를 pmap
제공 할 수 있습니다.
dots = pmap(dot, 1:10, 11:20)
그러나 @parallel
과 마찬가지로 pmap
주어진 함수가 Julia에 있지 않은 경우 (즉, 사용자 정의 또는 패키지로 정의 된 경우) 먼저 모든 작업자가 함수를 사용할 수 있는지 확인해야합니다.
@everywhere begin
function rand_det(n)
det(rand(n,n))
end
end
determinants = pmap(rand_det, 1:10)
이 SO Q & A도 참조하십시오.
@평행
@parallel을 사용하여 루프를 병렬 처리하여 루프의 단계를 다른 작업자로 나눌 수 있습니다. 아주 간단한 예로서 :
addprocs(3)
a = collect(1:10)
for idx = 1:10
println(a[idx])
end
약간 더 복잡한 예제의 경우 다음을 고려하십시오.
@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
따라서 우리는 @parallel
루프를 실행 @parallel
27 초가 걸리는 대신 55 초가 걸린다는 것을 알 수 있습니다.
또한 @parallel
매크로에 대해 감소 연산자를 제공 할 수도 있습니다. 배열이 있다고 가정하고, 배열의 각 열을 합한 다음이 합계를 서로 곱하려고합니다.
A = rand(100,100);
@parallel (*) for idx = 1:size(A,1)
sum(A[:,idx])
end
예기치 않은 동작을 피하기 위해 @parallel
을 사용할 때 유의해야 할 몇 가지 중요한 사항이 있습니다.
첫째 , 기본 줄리아에없는 함수 (예 : 스크립트에서 정의한 함수 또는 패키지에서 가져 오는 함수)를 루프에서 사용할 경우에는 해당 함수를 액세스 가능하게해야합니다. 예를 들어, 다음과 같이 작동 하지 않습니다 .
myprint(x) = println(x)
for idx = 1:10
myprint(a[idx])
end
대신 다음을 사용해야합니다.
@everywhere begin
function myprint(x)
println(x)
end
end
@parallel for idx in 1:length(a)
myprint(a[idx])
end
둘째 , 각 작업자는 컨트롤러의 범위에있는 객체에 액세스 할 수 있지만 수정할 수는 없습니다 . 그러므로
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
우리가 @parallel wihtout 루프를 실행 한 경우, 반면에 성공적 배열 수정 한 것 a
.
a
하기 위해 대신 SharedArray
유형 객체를 만들어 각 작업자가 액세스하고 수정할 수 있도록 할 수 있습니다.
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 및 @spawnat
@spawn
및 @spawnat
매크로는 Julia가 작업자에게 작업을 할당하는 데 사용할 수있는 두 가지 도구입니다. 다음은 그 예입니다.
julia> @spawnat 2 println("hello world")
RemoteRef{Channel{Any}}(2,1,3)
julia> From worker 2: hello world
이 두 매크로는 작업자 프로세스에서 표현식 을 평가합니다. 두 가지의 유일한 차이점은 @spawnat
사용하면 표현식을 평가할 작업자 (작업자 2 위의 예에서 지정)를 선택할 수 있으며 반면에 @spawn
하면 작업자가 가용성에 따라 자동으로 선택됩니다.
위의 예제에서 worker 2는 println 함수를 실행 시켰을뿐입니다. 거기에서 돌아 오거나 검색 할 관심이 없습니다. 그러나 종종 우리가 근로자에게 보낸 표현은 우리가 찾고자하는 것을 만들어 낼 것입니다. 위의 예제에서 @spawnat
을 호출했을 때 worker 2의 출력물을 얻기 전에 다음과 같은 것을 보았습니다 :
RemoteRef{Channel{Any}}(2,1,3)
이것은 @spawnat
매크로가 RemoteRef
유형 객체를 반환한다는 것을 나타냅니다. 이 객체에는 작업자에게 전송되는 표현식의 반환 값이 차례로 포함됩니다. 이러한 값을 검색하려면 먼저 @spawnat
이 객체에 반환 한 RemoteRef
를 할당 한 다음 RemoteRef
유형 객체에서 작동하는 fetch()
함수를 사용하여에 수행 된 평가에서 저장된 결과를 검색 할 수 있습니다 노동자.
julia> result = @spawnat 2 2 + 5
RemoteRef{Channel{Any}}(2,1,26)
julia> fetch(result)
7
@spawn
효과적으로 사용할 수있는 열쇠는 그것이 작동하는 표현식 의 본질을 이해하는 것입니다. @spawn
을 사용하여 @spawn
에게 명령을 보내면 작업자 중 하나에게 "인터프리터"를 실행하거나 코드를 네이티브로 실행하는 경우 입력 할 내용을 직접 입력하는 것보다 약간 더 복잡합니다. 예를 들어 @spawnat
을 사용하여 작업자의 변수에 값을 할당하고자한다고 가정합니다. 시도해 볼 수 있습니다.
@spawnat 2 a = 5
RemoteRef{Channel{Any}}(2,1,2)
작동 했나요? 작업자 2가 인쇄를 시도하도록 a
.
julia> @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,4)
julia>
아무 일도하지. 왜? 위와 같이 fetch()
를 사용하여 더 많은 것을 조사 할 수 있습니다. fetch()
는 성공한 결과뿐만 아니라 오류 메시지도 검색하기 때문에 매우 편리 할 수 있습니다. 그것 없이는 우리는 무언가가 잘못되었다는 것을 알지 못할 수도 있습니다.
julia> result = @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,5)
julia> fetch(result)
ERROR: On worker 2:
UndefVarError: a not defined
오류 메시지는 a
가 worker 2에 정의되어 있지 않다고 말합니다. 그 이유는 할당 작업을 표현식으로 래핑하여 작업자에게 평가하도록 @spawn
을 사용하기 @spawn
입니다. 다음은 설명과 함께 예제입니다.
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
:()
구문은 Julia가 표현식 을 지정 하는 데 사용하는 구문입니다. 그런 다음 Julia에서 eval()
함수를 사용하여 표현식을 평가하고 @spawnat
매크로를 사용하여 표현식이 worker 2에서 평가되도록 지시합니다.
다음과 같은 결과를 얻을 수도 있습니다.
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
이 예제는 두 가지 추가 개념을 보여줍니다. 먼저 문자열에서 호출되는 parse()
함수를 사용하여 표현식을 만들 수 있음을 알 수 있습니다. 둘째로, 구문이보다 명확하고 관리하기 쉬운 상황에서 @spawnat
호출 할 때 괄호를 사용할 수 있음을 알 수 있습니다.
@parallel 대 pmap을 사용하는 경우
줄리아 문서 는
pmap ()은 각 함수 호출이 많은 양의 작업을 수행하는 경우를 위해 설계되었습니다. 대조적으로, @ for parallel은 각 반복이 작은 상황을 처리 할 수 있습니다. 아마도 두 숫자를 합하는 것일 것입니다.
이것에는 몇 가지 이유가 있습니다. 첫째, pmap
은 작업 시작 비용을 더 많이 발생시킵니다. 따라서 작업이 매우 작 으면 이러한 시동 비용이 비효율적 일 수 있습니다. 반대로, pmap
은 작업자에게 작업을 할당하는 "더 똑똑한"작업을 수행합니다. 특히 작업 큐를 만들고 해당 작업자가 사용할 수있게 될 때마다 새로운 작업을 각 작업자에게 보냅니다. @parallel
대비로는,이 호출 될 때 노동자들 사이에 할 수있는 모든 일을 divvies. 따라서 일부 근로자가 다른 근로자보다 업무에 더 오래 걸리는 경우, 대부분의 근로자가 끝나고 유휴 상태에있는 상황이 발생할 수 있으며, 일부 근로자는 과도한 시간 동안 계속 활동하여 업무를 끝내게됩니다. 그러나 이러한 상황은 매우 작고 단순한 작업에서는 거의 발생하지 않습니다.
다음은 이것을 설명합니다. 두 명의 근로자가 있고 그 중 하나는 느리고 다른 하나는 2 배 빠릅니다. 이상적으로는, 우리는 빠른 작업자에게 느린 작업자보다 두 배나 많은 작업을 제공하고자합니다. (또는 우리는 일을 천천히하고 느릴 수 있지만 교장은 똑같습니다). pmap
이이를 수행하지만 @parallel
은 수행하지 않습니다.
각 테스트에 대해 다음을 초기화합니다.
addprocs(2)
@everywhere begin
function parallel_func(idx)
workernum = myid() - 1
sleep(workernum)
println("job $idx")
end
end
이제 @parallel
테스트를 위해 다음을 실행합니다.
@parallel for idx = 1:12
parallel_func(idx)
end
그리고 출력물을 다시 받으십시오 :
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
거의 다 멋지 네요. 노동자들은 노동을 균등하게 "공유"했다. 작업자 2가 작업자 3의 두 배 빠른 속도 임에도 불구하고 각 작업자는 6 개의 작업을 완료했습니다. 이는 손댈 수는 있지만 비효율적입니다.
pmap
테스트에서 다음을 실행합니다.
pmap(parallel_func, 1:12)
출력을 얻는다.
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
이제 작업자 2는 8 개의 작업을 수행하고 작업자 3은 4 개의 작업을 수행했습니다. 이는 속도에 비례하고 최적의 효율성을 위해 원하는 것입니다. pmap
은 각각의 능력에 따라 어려운 작업 마스터입니다.
@async 및 @sync
?@async
아래의 문서에 따르면 " @async
는 작업에서 표현식을 래핑합니다." 이것이 의미하는 바는 Julia가이 작업을 실행하기 시작하지만 작업 완료를 기다리지 않고 스크립트에서 다음에 오는 작업으로 넘어갑니다. 예를 들어 매크로가 없으면 다음과 같은 결과를 얻습니다.
julia> @time sleep(2)
2.005766 seconds (13 allocations: 624 bytes)
그러나 매크로를 사용하면 다음과 같은 이점을 얻을 수 있습니다.
julia> @time @async sleep(2)
0.000021 seconds (7 allocations: 657 bytes)
Task (waiting) @0x0000000112a65ba0
julia>
Julia는 작업 (이 경우에는 2 초 동안 잠자다)이 끝나기를 기다리지 않고 스크립트가 진행되도록 (그리고 @time
매크로가 완전히 실행되도록) 허용합니다.
반대로 @sync
매크로는 @async
, @spawn
, @spawnat
및 @parallel
의 모든 동적 닫힌 사용이 완료 될 때까지 대기합니다. ( ?@sync
아래의 문서에 따라). 따라서 우리는 다음을 봅니다 :
julia> @time @sync @async sleep(2)
2.002899 seconds (47 allocations: 2.986 KB)
Task (done) @0x0000000112bd2e00
이 간단한 예제에서 @async
와 @sync
의 단일 인스턴스를 함께 포함 할 필요는 없습니다. 그러나 @sync
가 유용 할 수있는 곳은 @async
를 여러 작업에 적용하여 각 작업이 완료 될 때까지 기다리지 않고 동시에 시작할 수 있도록하려는 것입니다.
예를 들어, 여러 명의 직원이 있고 각각의 직원이 동시에 작업을 시작한 다음 해당 작업의 결과를 가져 오려고한다고 가정합니다. 초기 (그러나 올바르지 않은) 시도는 다음과 같을 수 있습니다.
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)
여기서 문제는 각 remotecall_fetch()
작업이 완료 될 때 remotecall_fetch()
기다리는 것입니다. 즉, 각 프로세스가 작업을 완료 (이 경우에는 2 초 동안 remotecall_fetch()
)하기 전에 remotecall_fetch()
다음 remotecall_fetch()
작업을 계속합니다. 실용적인 측면에서 우리는 병렬 처리의 이점을 얻지 못하고 있습니다. 왜냐하면 프로세스가 동시에 작업 (즉, 수면)을 수행하지 않기 때문입니다.
그러나 @sync
매크로와 @sync
매크로를 조합 @async
를 @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)
이제 루프의 각 단계를 별도의 연산으로 계산하면 @async
매크로 앞에 두 개의 개별 연산이 있음을 @async
있습니다. 매크로는 각각이 시작하기 전에 코드를 시작하고 (이 경우에는 루프의 다음 단계로 진행) 코드가 끝나기 전에 계속 진행합니다. 그러나 범위가 전체 루프를 포함하는 @sync
매크로를 사용한다는 것은 @async
가 선행 된 모든 작업이 완료 될 때까지 스크립트가 해당 루프를지나 진행할 수 없음을 의미합니다.
위의 예제를 수정하여 특정 매크로를 변경하면 매크로가 어떻게 작동하는지 훨씬 더 명확하게 이해할 수 있습니다. 예를 들어 @async
없는 @async
가 있다고 가정 @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)
여기서 @async
매크로는 각 remotecall_fetch()
작업이 실행되기 전에도 우리의 루프를 계속할 수있게 해줍니다. 그러나 더 좋든 나쁘 든간에 모든 remotecall_fetch()
작업이 끝날 때까지 코드가이 루프를 지나지 않게하는 @sync
매크로가 없습니다.
그럼에도 불구하고 각각의 remotecall_fetch()
작업은 우리가 작업을 계속하더라도 여전히 병렬로 실행됩니다. 우리는 2 초 동안 기다리면 그 결과를 담고있는 배열 a가 다음을 포함 할 것이기 때문에 그것을 볼 수 있습니다 :
sleep(2)
julia> a
2-element Array{Any,1}:
nothing
nothing
( "nothing"요소는 어떤 값도 반환하지 않는 sleep 함수의 결과를 성공적으로 가져온 결과입니다)
또한 두 개의 remotecall_fetch()
작업이 본질적으로 동시에 시작된다는 것을 알 수 있습니다. 그 이유는 앞에 나온 print
명령이 빠른 연속적으로 실행되기 때문입니다 (여기에 표시되지 않은 명령의 출력). 이것을 print
명령이 서로 2 초 지연하여 실행되는 다음 예제와 대조하십시오.
@async
매크로를 (내부 단계가 아닌) 전체 루프에 remotecall_fetch()
작업이 끝날 때까지 기다리지 않고 스크립트가 즉시 계속됩니다. 그러나 이제는 스크립트가 전체 루프를지나 계속 진행할 수 있습니다. 이전 루프가 완료되기 전에 루프의 각 개별 단계를 시작할 수 없습니다. 따라서 위 예제와 달리 스크립트가 루프 다음에 진행된 후 2 초가 지나면 results
배열에는 여전히 두 번째 remotecall_fetch()
작업이 아직 완료되지 않았 음을 나타내는 #undef
라는 요소가 하나씩 있습니다.
@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
그리고 놀랍지 않게도 @sync
와 @async
를 서로 옆에 remotecall_fetch()
가 순차적으로 (동시에 실행되는 것이 아니라 remotecall_fetch()
실행되지만 각 작업이 완료 될 때까지 코드에서 계속 수행하지 않습니다. 다른 말로하면 sleep(2)
가 @sync @async sleep(2)
본질적으로 동일하게 동작하는 것처럼 매크로가 @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
@async
매크로의 범위 내에서 더 복잡한 연산을 수행 할 수도 있습니다. 이 문서 는 @async
범위 내에서 전체 루프를 포함하는 예제를 제공합니다.
동기화 매크로에 대한 도움말에는 " @async
, @spawn
, @spawnat
및 @parallel
의 모든 동적 닫힌 사용이 완료 될 때까지 기다릴 것"이라고 명시되어 있습니다. "완료"로 간주되는 목적을 위해 @sync
및 @async
매크로의 범위 내에서 작업을 정의하는 방법이 중요합니다. 위에 주어진 예제 중 하나에 약간의 변형이있는 아래 예제를 고려하십시오.
@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)
앞의 예제는 실행에 약 2 초가 걸렸습니다. 두 작업이 병렬로 실행되었고 스크립트가 진행하기 전에 함수 실행을 완료하기를 기다리고 있음을 나타냅니다. 그러나이 예제에는 시간 평가가 훨씬 적습니다. 그 이유는 @sync
의 목적을 위해 remotecall()
작업은 작업자에게 작업을 보낸 후에 "완료"했습니다. 결과 배열 (여기에서 a는 RemoteRef
객체 유형을 포함하는데, 이는 이론적으로 미래의 어떤 시점에서 가져올 수있는 특정 프로세스에서 일어나는 일이 있음을 나타냅니다.) 반대로 작업자가 작업이 완료되었다는 메시지를 받으면 remotecall_fetch()
작업은 "완료"됩니다.
따라서 직원들과의 특정 작업이 스크립트에서 계속 진행되기 전에 완료되었는지 확인하는 방법을 찾고 있다면 (예 : 이 게시물 에서 논의 됨) 무엇이 "완료"로 간주되는지 그리고 어떻게 완료 될지에 대해 신중하게 생각할 필요가 있습니다 스크립트에서이를 측정하고 조작하십시오.
근로자 추가
Julia를 처음 시작하면 기본적으로 하나의 프로세스 만 실행되고 작업 할 수 있습니다. 다음을 사용하여이를 확인할 수 있습니다.
julia> nprocs()
1
병렬 처리를 이용하려면 먼저 추가 작업자를 추가해야합니다. 그러면 추가 작업자가 할당 된 작업을 수행 할 수 있습니다. 스크립트 (또는 인터프리터)에서 다음을 사용하여이를 수행 할 수 있습니다. addprocs(n)
여기서 n
은 사용하려는 프로세스의 수입니다.
또는 다음을 사용하여 명령 줄에서 Julia를 시작할 때 프로세스를 추가 할 수 있습니다.
$ julia -p n
여기서 n
은 추가 할 프로세스의 수입니다. 따라서 우리가 줄리아를 시작하면
$ julia -p 2
Julia가 시작되면 우리는 다음을 얻습니다 :
julia> nprocs()
3