Как говорит Билл, в Julia есть два основных способа думать о параллелизме: многопоточная модель, которая была представлена в Julia 1.3 и выполняет параллелизм совместно используемой памяти через макрос Threads.@threads
, и распределенная обработка с использованием макроса Distributed.@distributed
, который распараллеливает различные процессы Юлии.
Threads определенно ближе к ускорению параллелизма "automagi c" с минимальным перезаписыванием кода или вообще без него и часто отличным вариантом, хотя нужно позаботиться о том, чтобы какая бы операция ни выполнялась работа является потокобезопасной, поэтому всегда проверяйте, чтобы результаты были одинаковыми.
Поскольку ваш вопрос изначально касался @distributed
параллелизма, позвольте мне также ответить на него. Если вы выполняете @distributed
параллелизм, самая простая ментальная модель (я полагаю), чтобы подумать о том, что происходит, состоит в том, чтобы представить, что вы выполняете свой код в совершенно отдельных REPL Джулии.
Вот версия вашего кода, адаптированная для модели @distributed
:
using Distributed
addprocs(2)
using SharedArrays
using DelimitedFiles
@everywhere begin
using DifferentialEquations
tf(x,w) = x*sin(w*x)
function sys!(dv,v,w,t)
dv[1] = w*v[1]
dv[2] = tf(v[1],w)
end
times = LinRange(0.1,2,25)
params = LinRange(0.1,1.2,100)
end
sols = SharedArray{Float64,2}((length(times),length(params)))
@sync @distributed for i=1:length(params)
println(i)
init_val = [1.0,1.0]
tspan = (0.0,2.0)
prob = ODEProblem(sys!,init_val,tspan,params[i])
sol = solve(prob)
sols[:,i] .= sol(times)[2,:]
end
sols
Что изменилось?
Я добавил addprocs(2)
в начале сценария. В этом нет необходимости, если вы запускаете Julia с p -2
(или с любым числом процессов, которое вы хотите), как вы это делаете, но мне часто бывает проще рассуждать о коде, когда он явно устанавливает параллельную среду в коде напрямую , Обратите внимание, что в настоящее время это невозможно для потоков, т. Е. Вам нужно установить переменную окружения JULIA_NUM_THREADS
перед запуском Julia, и вы не можете изменить количество потоков после запуска и запуска.
Затем я переместил биты кода в блок @everywhere begin ... end
. Это, по сути, запускает операции, включенные в блок, для всех процессов одновременно. Возвращаясь к ментальной модели запуска отдельных экземпляров Julia, вы должны посмотреть, что находится в вашем @distributed
l oop, и убедиться, что все функции и переменные действительно определены во всех процессах. Так, например, чтобы каждый процесс знал, что такое ODEProblem
, вам нужно сделать using DifferentialEquations
для всех из них.
Наконец, я добавил @sync
к распределенному l oop. На это ссылаются в документах для @distributed
. Запуск макроса @distributed
с for
l oop порождает дескриптор асинхронного зеленого потока (Task
) для распределенного выполнения и перемещается вперед на следующую строку. Поскольку вы хотите подождать до фактического завершения выполнения, требуется синхронизация @sync
. Проблема с вашим исходным кодом заключается в том, что, не дожидаясь завершения зеленого потока (синхронизации), он будет проглатывать ошибки и просто сразу же возвращаться, поэтому ваш массив sol
пуст. Это можно увидеть, если вы запустите исходный код и добавите только @sync
- тогда вы получите TaskFailedException: on worker 2 - UndefVarError: #sys! not defined
, который сообщит вам, что ваши рабочие процессы не знают о функциях, которые вы определили в главном процессе. На практике вам почти всегда нужно выполнение @sync
, если только вы не планируете запускать много таких распределенных циклов параллельно. Вам также не нужно ключевое слово @sync
, где вы используете агрегаторную функцию в распределенном l oop (@distributed (func) for i in 1:1000
форма l oop)
Теперь что такое лучшее решение здесь? Ответ - я не знаю. @threads
является отличным вариантом для быстрого распараллеливания поточно-ориентированной работы без переписывания кода, и он все еще активно разрабатывается и совершенствуется, так что, вероятно, станет еще лучше в будущем. В стандартной библиотеке Distributed есть также pmap
, который дает вам дополнительные опции, но этот ответ достаточно длинный! По моему личному опыту, ничто не заменит (1) размышления о вашей проблеме и (2) выполнение сравнительного анализа. Вы должны подумать о времени выполнения вашей проблемы (как общей, так и для каждой отдельной операции, которую вы хотите распространить) и требованиях к передаче сообщений / доступу к памяти.
Плюс в том, что, хотя вам, возможно, придется потратить немного усилий на размышления о вещах, у Джулии есть куча отличных вариантов, позволяющих максимально эффективно использовать любую аппаратную ситуацию на старом дрянном ноутбуке с двумя ядрами (например, тот, который я набираю это из) в многоузловые сверхвысокопроизводительные кластеры (что сделало Джулию одним из очень немногих языков программирования для достижения производительности в петафлопе - хотя, честно говоря, это немного сложнее, чем мой или Ответ Билла :))