Multi-processing and Distributed Computing
يتم توفير تنفيذ للحوسبة المتوازية في الذاكرة الموزعة بواسطة الوحدة Distributed
كجزء من المكتبة القياسية المرفقة مع جوليا.
تمتلك معظم أجهزة الكمبيوتر الحديثة أكثر من وحدة معالجة مركزية واحدة، ويمكن دمج عدة أجهزة كمبيوتر معًا في مجموعة. يسمح استغلال قوة هذه المعالجات المتعددة بإكمال العديد من الحسابات بشكل أسرع. هناك عاملان رئيسيان يؤثران على الأداء: سرعة وحدات المعالجة المركزية نفسها، وسرعة وصولها إلى الذاكرة. في مجموعة، من الواضح إلى حد ما أن وحدة معالجة مركزية معينة ستتمتع بأسرع وصول إلى ذاكرة الوصول العشوائي داخل نفس الكمبيوتر (العقدة). ربما يكون من المفاجئ أكثر أن القضايا المماثلة ذات صلة على جهاز كمبيوتر محمول متعدد النواة نموذجي، بسبب الاختلافات في سرعة الذاكرة الرئيسية و cache. وبالتالي، يجب أن يسمح بيئة المعالجة المتعددة الجيدة بالتحكم في "ملكية" جزء من الذاكرة بواسطة وحدة معالجة مركزية معينة. توفر جوليا بيئة معالجة متعددة تعتمد على تمرير الرسائل للسماح للبرامج بالعمل على عمليات متعددة في مجالات ذاكرة منفصلة في وقت واحد.
تنفيذ جوليّا لتمرير الرسائل يختلف عن البيئات الأخرى مثل MPI[1]. التواصل في جوليّا عمومًا "من جانب واحد"، مما يعني أن المبرمج يحتاج إلى إدارة عملية واحدة فقط بشكل صريح في عملية تتضمن عمليتين. علاوة على ذلك، فإن هذه العمليات عادةً لا تبدو مثل "إرسال رسالة" و"استقبال رسالة" بل تشبه عمليات أعلى مستوى مثل استدعاءات لدوال المستخدم.
تم بناء البرمجة الموزعة في جوليا على اثنين من البدائيات: المراجع البعيدة و المكالمات البعيدة. المراجع البعيدة هي كائن يمكن استخدامه من أي عملية للإشارة إلى كائن مخزن على عملية معينة. المكالمة البعيدة هي طلب من عملية واحدة لاستدعاء دالة معينة مع معطيات معينة على عملية أخرى (ربما تكون هي نفسها).
تأتي المراجع البعيدة بنوعين: Future
و RemoteChannel
.
تُعيد المكالمة البعيدة Future
إلى نتيجتها. تُعيد المكالمات البعيدة على الفور؛ حيث يتقدم العملية التي أجرت المكالمة إلى عمليتها التالية بينما تحدث المكالمة البعيدة في مكان آخر. يمكنك الانتظار حتى تنتهي المكالمة البعيدة عن طريق استدعاء wait
على 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
المُعاد، ويمكنك الحصول على القيمة الكاملة للنتيجة باستخدام fetch
.
من ناحية أخرى، RemoteChannel
قابلة لإعادة الكتابة. على سبيل المثال، يمكن لعمليات متعددة تنسيق معالجتها من خلال الإشارة إلى نفس Channel
البعيد.
كل عملية لها معرف مرتبط بها. العملية التي توفر موجه جوليا التفاعلي دائمًا ما يكون لها id
يساوي 1. تُعرف العمليات المستخدمة افتراضيًا للعمليات المتوازية باسم "العمال". عندما تكون هناك عملية واحدة فقط، تعتبر العملية 1 عاملاً. خلاف ذلك، تعتبر العمال جميع العمليات الأخرى بخلاف العملية 1. نتيجة لذلك، يتطلب الأمر إضافة عمليتين أو أكثر للاستفادة من طرق المعالجة المتوازية مثل pmap
. إضافة عملية واحدة فقط تكون مفيدة إذا كنت ترغب فقط في القيام بأشياء أخرى في العملية الرئيسية بينما يتم تشغيل حساب طويل على العامل.
لنبدأ بذلك. بدءًا من julia -p n
يوفر n
عمليات عاملية على الجهاز المحلي. بشكل عام، من المنطقي أن يكون n
مساويًا لعدد خيوط وحدة المعالجة المركزية (النوى المنطقية) على الجهاز. لاحظ أن وسيط -p
يقوم بتحميل الوحدة Distributed
بشكل ضمني.
$ 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
الحجة الأولى إلى remotecall
هي الوظيفة التي سيتم استدعاؤها. معظم البرمجة المتوازية في جوليا لا تشير إلى عمليات محددة أو عدد العمليات المتاحة، ولكن 4d61726b646f776e2e436f64652822222c202272656d6f746563616c6c2229_40726566
تعتبر واجهة منخفضة المستوى توفر تحكمًا أدق. الحجة الثانية إلى 4d61726b646f776e2e436f64652822222c202272656d6f746563616c6c2229_40726566
هي id
العملية التي ستقوم بالعمل، وستمرر الحجج المتبقية إلى الوظيفة التي يتم استدعاؤها.
كما ترى، في السطر الأول طلبنا من العملية 2 إنشاء مصفوفة عشوائية بحجم 2×2، وفي السطر الثاني طلبنا منها إضافة 1 إليها. نتيجة كلا الحسابين متاحة في المستقبلين، r
و s
. ماكرو @spawnat
يقوم بتقييم التعبير في الحجة الثانية على العملية المحددة في الحجة الأولى.
أحيانًا قد ترغب في الحصول على قيمة محسوبة عن بُعد على الفور. يحدث هذا عادةً عندما تقرأ من كائن بعيد للحصول على البيانات اللازمة للعملية المحلية التالية. الدالة remotecall_fetch
موجودة لهذا الغرض. إنها تعادل fetch(remotecall(...))
لكنها أكثر كفاءة.
julia> remotecall_fetch(r-> fetch(r)[1, 1], 2, r)
0.18526337335308085
هذا يسترجع المصفوفة على العامل 2 ويعيد القيمة الأولى. لاحظ أن fetch
لا ينقل أي بيانات في هذه الحالة، لأنه يتم تنفيذه على العامل الذي يمتلك المصفوفة. يمكن أيضًا كتابة:
julia> remotecall_fetch(getindex, 2, r, 1, 1)
0.10824216411304866
تذكر أن getindex(r,1,1)
هو equivalent إلى r[1,1]
، لذا فإن هذا الاستدعاء يجلب العنصر الأول من المستقبل r
.
لتسهيل الأمور، يمكن تمرير الرمز :any
إلى @spawnat
، الذي يحدد مكان إجراء العملية لك:
julia> r = @spawnat :any rand(2,2)
Future(2, 1, 4, nothing)
julia> s = @spawnat :any 1 .+ fetch(r)
Future(3, 1, 5, nothing)
julia> fetch(s)
2×2 Array{Float64,2}:
1.38854 1.9098
1.20939 1.57158
لاحظ أننا استخدمنا 1 .+ fetch(r)
بدلاً من 1 .+ r
. وذلك لأننا لا نعرف أين سيتم تشغيل الكود، لذا بشكل عام قد يكون من الضروري استخدام fetch
لنقل r
إلى العملية التي تقوم بالجمع. في هذه الحالة، @spawnat
ذكي بما يكفي لتنفيذ الحساب على العملية التي تمتلك r
، لذا سيكون 4d61726b646f776e2e436f64652822222c202266657463682229_40726566
عملية لا تفعل شيئًا (لا يتم القيام بأي عمل).
(من الجدير بالذكر أن @spawnat
ليس مدمجًا ولكن تم تعريفه في جوليا كـ macro. من الممكن تعريف مثل هذه البنى الخاصة بك.)
من المهم أن نتذكر أنه، بمجرد استرجاعه، سيتم تخزين قيمة Future
محليًا. لا تتطلب المكالمات اللاحقة لـ fetch
أي انتقال عبر الشبكة. بمجرد أن يتم استرجاع جميع القيم المرجعية لـ 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
، يتم حذف القيمة المخزنة عن بُعد.
@async
مشابه لـ @spawnat
، ولكنه يقوم بتشغيل المهام فقط على العملية المحلية. نستخدمه لإنشاء مهمة "مغذي" لكل عملية. تختار كل مهمة الفهرس التالي الذي يحتاج إلى حساب، ثم تنتظر حتى تنتهي عمليتها، ثم تعيد العملية حتى نفاد الفهارس. لاحظ أن مهام المغذي لا تبدأ في التنفيذ حتى تصل المهمة الرئيسية إلى نهاية كتلة @sync
، في هذه النقطة تتخلى عن السيطرة وتنتظر حتى تكتمل جميع المهام المحلية قبل العودة من الدالة. بالنسبة للإصدارات 0.7 وما بعدها، فإن مهام المغذي قادرة على مشاركة الحالة عبر nextidx
لأنها جميعًا تعمل على نفس العملية. حتى إذا تم جدولة Tasks
بشكل تعاوني، قد لا يزال من الضروري استخدام القفل في بعض السياقات، كما في asynchronous I/O. هذا يعني أن تبديلات السياق تحدث فقط في نقاط محددة جيدًا: في هذه الحالة، عندما يتم استدعاء remotecall_fetch
. هذه هي الحالة الحالية للتنفيذ وقد تتغير في إصدارات جوليا المستقبلية، حيث من المقصود أن تجعل من الممكن تشغيل ما يصل إلى N Tasks
على M Process
، المعروف أيضًا بـ M:N Threading. ثم سيكون نموذج الحصول على القفل\إطلاقه لـ nextidx
مطلوبًا، حيث أنه ليس من الآمن السماح لعمليتين بقراءة وكتابة مورد في نفس الوقت.
Code Availability and Loading Packages
يجب أن يكون كودك متاحًا في أي عملية تقوم بتشغيله. على سبيل المثال، اكتب ما يلي في موجه جوليا:
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(@spawnat :any rand2(2,2))
ERROR: RemoteException(2, CapturedException(UndefVarError(Symbol("#rand2"))))
Stacktrace:
[...]
كانت العملية 1 تعرف عن الدالة rand2
، لكن العملية 2 لم تكن تعرف عنها.
عادةً، ستقوم بتحميل الشيفرة من الملفات أو الحزم، ولديك قدر كبير من المرونة في التحكم في العمليات التي تقوم بتحميل الشيفرة. اعتبر ملفًا، DummyModule.jl
، يحتوي على الشيفرة التالية:
module DummyModule
export MyType, f
mutable struct MyType
a::Int
end
f(x) = x^2+1
println("loaded")
end
لكي يتم الإشارة إلى MyType
عبر جميع العمليات، يجب تحميل DummyModule.jl
على كل عملية. استدعاء include("DummyModule.jl")
يقوم بتحميله فقط على عملية واحدة. لتحميله على كل العمليات، استخدم الماكرو @everywhere
(ابدأ جوليا باستخدام julia -p 2
):
julia> @everywhere include("DummyModule.jl")
loaded
From worker 3: loaded
From worker 2: loaded
كما هو معتاد، لا يجلب هذا DummyModule
إلى النطاق في أي من العمليات، مما يتطلب using
أو import
. علاوة على ذلك، عندما يتم جلب DummyModule
إلى النطاق في عملية واحدة، فإنه لا يكون في أي عملية أخرى:
julia> using .DummyModule
julia> MyType(7)
MyType(7)
julia> fetch(@spawnat 2 MyType(7))
ERROR: On worker 2:
UndefVarError: `MyType` not defined in `Main`
⋮
julia> fetch(@spawnat 2 DummyModule.MyType(7))
MyType(7)
ومع ذلك، لا يزال من الممكن، على سبيل المثال، إرسال MyType
إلى عملية قامت بتحميل DummyModule
حتى لو لم يكن في النطاق:
julia> put!(RemoteChannel(2), MyType(7))
RemoteChannel{Channel{Any}}(2, 1, 13)
يمكن أيضًا تحميل ملف على عمليات متعددة عند بدء التشغيل باستخدام علامة -L
، ويمكن استخدام برنامج تشغيل لدفع الحساب:
julia -p <n> -L file1.jl -L file2.jl driver.jl
تتمتع عملية جوليا التي تشغل نص السائق في المثال أعلاه بمعرف (id
) يساوي 1، تمامًا مثل عملية تقدم موجه تفاعلي.
أخيرًا، إذا لم يكن DummyModule.jl
ملفًا مستقلًا بل حزمة، فإن using DummyModule
سيقوم بتحميل DummyModule.jl
على جميع العمليات، ولكنه سيجلبه إلى النطاق فقط في العملية التي تم فيها استدعاء using
.
Starting and managing worker processes
تحتوي تثبيت جوليا الأساسي على دعم مدمج لنوعين من الكتل:
- عناقيد محلية محددة باستخدام خيار
-p
كما هو موضح أعلاه. - عناقيد تمتد عبر الآلات باستخدام خيار
--machine-file
. يستخدم هذا تسجيل دخولssh
بدون كلمة مرور لبدء عمليات العامل في جوليا (من نفس المسار مثل المضيف الحالي) على الآلات المحددة. يأخذ تعريف كل آلة الشكل[count*][user@]host[:port] [bind_addr[:port]]
. الافتراضي لـuser
هو المستخدم الحالي، وport
هو منفذ ssh القياسي.count
هو عدد العمال الذين سيتم إنشاؤهم على العقدة، والافتراضي هو 1. يحدد الخيار الاختياريbind-to bind_addr[:port]
عنوان IP والمنفذ الذي يجب أن تستخدمه العمال الآخرون للاتصال بهذا العامل.
بينما تسعى جوليا عمومًا للحفاظ على التوافق مع الإصدارات السابقة، يعتمد توزيع الشيفرة إلى عمليات العمل على Serialization.serialize
. كما هو مذكور في الوثائق المقابلة، لا يمكن ضمان أن يعمل هذا عبر إصدارات جوليا المختلفة، لذا يُنصح بأن تستخدم جميع العمال على جميع الآلات نفس الإصدار.
تتوفر الدوال addprocs
، rmprocs
، workers
، وغيرها كوسيلة برمجية لإضافة وإزالة واستعلام العمليات في مجموعة.
julia> using Distributed
julia> addprocs(2)
2-element Array{Int64,1}:
2
3
يجب تحميل الوحدة Distributed
بشكل صريح على عملية الماستر قبل استدعاء addprocs
. وهي متاحة تلقائيًا على عمليات العامل.
لاحظ أن العمال لا يقومون بتشغيل برنامج بدء التشغيل ~/.julia/config/startup.jl
، ولا يقومون بمزامنة حالتهم العالمية (مثل خيارات سطر الأوامر، المتغيرات العالمية، تعريفات الطرق الجديدة، والوحدات المحملة) مع أي من العمليات الأخرى التي تعمل. يمكنك استخدام addprocs(exeflags="--project")
لتهيئة عامل ببيئة معينة، ثم @everywhere using <modulename>
أو @everywhere include("file.jl")
.
يمكن دعم أنواع أخرى من الكتل من خلال كتابة ClusterManager
مخصص خاص بك، كما هو موضح أدناه في قسم ClusterManagers.
Data Movement
إرسال الرسائل ونقل البيانات يشكلان معظم الحمل الزائد في برنامج موزع. تقليل عدد الرسائل وكمية البيانات المرسلة أمر حاسم لتحقيق الأداء وقابلية التوسع. لتحقيق هذه الغاية، من المهم فهم حركة البيانات التي تقوم بها مختلف هياكل البرمجة الموزعة في جوليا.
fetch
يمكن اعتباره عملية نقل بيانات صريحة، لأنه يطلب مباشرة نقل كائن إلى الجهاز المحلي. @spawnat
(وبعض البنى ذات الصلة) تنقل أيضًا البيانات، لكن هذا ليس واضحًا كما هو الحال، لذا يمكن تسميته عملية نقل بيانات ضمنية. اعتبر هذين النهجين لبناء ومربعة مصفوفة عشوائية:
الطريقة 1:
julia> A = rand(1000,1000);
julia> Bref = @spawnat :any A^2;
[...]
julia> fetch(Bref);
الطريقة 2:
julia> Bref = @spawnat :any rand(1000,1000)^2;
[...]
julia> fetch(Bref);
يبدو أن الفرق تافه، لكنه في الواقع مهم جدًا بسبب سلوك @spawnat
. في الطريقة الأولى، يتم إنشاء مصفوفة عشوائية محليًا، ثم تُرسل إلى عملية أخرى حيث يتم تربيعها. في الطريقة الثانية، يتم إنشاء مصفوفة عشوائية وتربيعها في عملية أخرى. لذلك، ترسل الطريقة الثانية بيانات أقل بكثير من الطريقة الأولى.
في هذا المثال البسيط، من السهل تمييز الطريقتين والاختيار بينهما. ومع ذلك، في برنامج حقيقي، قد يتطلب تصميم حركة البيانات مزيدًا من التفكير ومن المحتمل بعض القياسات. على سبيل المثال، إذا كانت العملية الأولى تحتاج إلى المصفوفة A
، فقد تكون الطريقة الأولى أفضل. أو، إذا كانت عملية حساب A
مكلفة وكان لدى العملية الحالية فقط، فقد يكون من غير الممكن تجنب نقلها إلى عملية أخرى. أو، إذا كانت العملية الحالية لديها القليل لتفعله بين @spawnat
و fetch(Bref)
، فقد يكون من الأفضل القضاء على التوازي تمامًا. أو تخيل أن rand(1000,1000)
تم استبداله بعملية أكثر تكلفة. عندها قد يكون من المنطقي إضافة بيان آخر 4d61726b646f776e2e436f64652822222c202240737061776e61742229_40726566
فقط لهذه الخطوة.
Global variables
يتم تنفيذ التعبيرات عن بُعد عبر @spawnat
، أو الإغلاقات المحددة للتنفيذ عن بُعد باستخدام remotecall
قد تشير إلى المتغيرات العالمية. يتم التعامل مع الربط العالمي تحت الوحدة Main
بشكل مختلف قليلاً مقارنةً بالربط العالمي في وحدات أخرى. اعتبر مقتطف الشيفرة التالي:
A = rand(10,10)
remotecall_fetch(()->sum(A), 2)
في هذه الحالة sum
يجب أن يتم تعريفه في العملية البعيدة. لاحظ أن A
هي متغير عالمي معرف في مساحة العمل المحلية. العامل 2 ليس لديه متغير يسمى A
تحت Main
. إن فعل شحن الإغلاق ()->sum(A)
إلى العامل 2 يؤدي إلى تعريف Main.A
على 2. تستمر Main.A
في الوجود على العامل 2 حتى بعد أن تعود المكالمة remotecall_fetch
. تدير المكالمات البعيدة مع المراجع العالمية المدمجة (تحت وحدة Main
فقط) المتغيرات العالمية كما يلي:
يتم إنشاء روابط عالمية جديدة على العمال الوجهة إذا تم الإشارة إليها كجزء من مكالمة عن بُعد.
تُعلن الثوابت العالمية كثوابت على العقد البعيدة أيضًا.
تُرسل المتغيرات العالمية إلى عامل الوجهة فقط في سياق استدعاء بعيد، وفقط إذا كانت قيمتها قد تغيرت. أيضًا، لا يقوم العنقود بمزامنة الربط العالمي عبر العقد. على سبيل المثال:
A = rand(10,10) remotecall_fetch(()->sum(A), 2) # worker 2 A = rand(10,10) remotecall_fetch(()->sum(A), 3) # worker 3 A = nothing
تنفيذ الشيفرة أعلاه يؤدي إلى أن
Main.A
على العامل 2 له قيمة مختلفة عنMain.A
على العامل 3، بينما قيمةMain.A
على العقدة 1 تم تعيينها إلىnothing
.
كما قد تكون أدركت، بينما يمكن جمع الذاكرة المرتبطة بالعالمات العامة عندما يتم إعادة تعيينها على الماستر، لا يتم اتخاذ أي إجراء من هذا القبيل على العمال حيث تظل الروابط صالحة. clear!
يمكن استخدامها لإعادة تعيين عالمات عامة محددة على العقد البعيدة إلى nothing
بمجرد عدم الحاجة إليها. سيؤدي ذلك إلى تحرير أي ذاكرة مرتبطة بها كجزء من دورة جمع القمامة العادية.
لذا يجب أن تكون البرامج حذرة عند الإشارة إلى المتغيرات العالمية في المكالمات عن بُعد. في الواقع، من الأفضل تجنبها تمامًا إذا كان ذلك ممكنًا. إذا كنت مضطرًا للإشارة إلى المتغيرات العالمية، فكر في استخدام كتل let
لتحديد المتغيرات العالمية محليًا.
على سبيل المثال:
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
كما يتضح، المتغير العالمي A
مُعرف على العامل 2، لكن B
مُلتقط كمتغير محلي وبالتالي لا يوجد ارتباط لـ B
على العامل 2.
Parallel Map and Loops
لحسن الحظ، لا تتطلب العديد من الحسابات المتوازية المفيدة نقل البيانات. مثال شائع هو محاكاة مونت كارلو، حيث يمكن لعمليات متعددة التعامل مع تجارب المحاكاة المستقلة في وقت واحد. يمكننا استخدام @spawnat
لقلب العملات على عمليتين. أولاً، اكتب الدالة التالية في count_heads.jl
:
function count_heads(n)
c::Int = 0
for i = 1:n
c += rand(Bool)
end
c
end
تقوم الدالة count_heads
ببساطة بجمع n
بتات عشوائية. إليك كيف يمكننا إجراء بعض التجارب على جهازين، وجمع النتائج معًا:
julia> @everywhere include_string(Main, $(read("count_heads.jl", String)), "count_heads.jl")
julia> a = @spawnat :any count_heads(100000000)
Future(2, 1, 6, nothing)
julia> b = @spawnat :any count_heads(100000000)
Future(3, 1, 7, nothing)
julia> fetch(a)+fetch(b)
100001564
هذا المثال يوضح نمط برمجة متوازية قوي وغالبًا ما يُستخدم. يتم تنفيذ العديد من التكرارات بشكل مستقل عبر عدة عمليات، ثم يتم دمج نتائجها باستخدام بعض الدوال. تُسمى عملية الدمج تخفيضًا، لأنها عادةً ما تقلل من رتبة الموتر: يتم تقليل متجه من الأرقام إلى رقم واحد، أو يتم تقليل مصفوفة إلى صف أو عمود واحد، إلخ. في الكود، يبدو هذا عادةً مثل النمط x = f(x,v[i])
، حيث x
هو المجمع، وf
هي دالة التخفيض، وv[i]
هي العناصر التي يتم تقليلها. من المرغوب فيه أن تكون f
تجميعية، بحيث لا يهم ترتيب تنفيذ العمليات.
لاحظ أن استخدامنا لهذا النمط مع count_heads
يمكن تعميمه. استخدمنا عبارتين صريحتين @spawnat
، مما يحد من التوازي إلى عمليتين. لتشغيله على أي عدد من العمليات، يمكننا استخدام حلقة for متوازية، تعمل في ذاكرة موزعة، والتي يمكن كتابتها في جوليا باستخدام @distributed
مثل هذا:
nheads = @distributed (+) for i = 1:200000000
Int(rand(Bool))
end
هذا البناء ينفذ نمط تخصيص التكرارات لعمليات متعددة، ودمجها مع تقليل محدد (في هذه الحالة (+)
). يتم أخذ نتيجة كل تكرار كقيمة للتعبير الأخير داخل الحلقة. التعبير الكامل للحلقة المتوازية نفسه يقيم إلى الإجابة النهائية.
لاحظ أنه على الرغم من أن حلقات التكرار المتوازية تبدو مثل حلقات التكرار التسلسلية، إلا أن سلوكها مختلف بشكل كبير. على وجه الخصوص، لا تحدث التكرارات بترتيب محدد، ولن تكون الكتابات إلى المتغيرات أو المصفوفات مرئية عالميًا نظرًا لأن التكرارات تعمل على عمليات مختلفة. سيتم نسخ أي متغيرات مستخدمة داخل الحلقة المتوازية وبثها إلى كل عملية.
على سبيل المثال، الكود التالي لن يعمل كما هو مقصود:
a = zeros(100000)
@distributed for i = 1:100000
a[i] = i
end
لن يقوم هذا الرمز بتهيئة جميع a
، حيث سيكون لكل عملية نسخة منفصلة منها. يجب تجنب حلقات for
المتوازية مثل هذه. لحسن الحظ، يمكن استخدام 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
على عينة مختارة عشوائيًا من متجه a
المشترك بين جميع العمليات.
كما يمكنك أن ترى، يمكن حذف عامل التخفيض إذا لم يكن مطلوبًا. في هذه الحالة، يتم تنفيذ الحلقة بشكل غير متزامن، أي أنها تطلق مهام مستقلة على جميع العمال المتاحين وتعيد مصفوفة من Future
على الفور دون الانتظار لإكمالها. يمكن للمتصل الانتظار لإكمال 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
في وقت لاحق عن طريق استدعاء fetch
عليها، أو الانتظار لإكمالها في نهاية الحلقة عن طريق تمييزها بـ @sync
، مثل @sync @distributed for
.
في بعض الحالات، لا حاجة إلى عامل تقليل، ونرغب ببساطة في تطبيق دالة على جميع الأعداد الصحيحة في نطاق ما (أو، بشكل أكثر عمومية، على جميع العناصر في مجموعة ما). هذه عملية مفيدة أخرى تُسمى الخريطة المتوازية، والتي تم تنفيذها في جوليا كدالة pmap
. على سبيل المثال، يمكننا حساب القيم الفردية لعدة مصفوفات عشوائية كبيرة بالتوازي كما يلي:
julia> M = Matrix{Float64}[rand(1000,1000) for i = 1:10];
julia> pmap(svdvals, M);
تم تصميم pmap
للحالة التي تتطلب فيها كل استدعاء دالة كمية كبيرة من العمل. بالمقابل، يمكن لـ @distributed for
التعامل مع الحالات التي تكون فيها كل تكرار صغيرة، ربما مجرد جمع عددين. يتم استخدام عمليات العمال فقط من قبل كل من 4d61726b646f776e2e436f64652822222c2022706d61702229_40726566
و @distributed for
من أجل الحساب المتوازي. في حالة @distributed for
، يتم إجراء التخفيض النهائي على عملية الاستدعاء.
Remote References and AbstractChannels
تشير المراجع البعيدة دائمًا إلى تنفيذ لـ AbstractChannel
.
يتطلب تنفيذ ملموس لـ AbstractChannel
(مثل Channel
) تنفيذ put!
، take!
، fetch
، isready
و wait
. يتم تخزين الكائن البعيد المشار إليه بواسطة Future
في Channel{Any}(1)
، أي Channel
بحجم 1 قادر على استيعاب كائنات من نوع Any
.
RemoteChannel
، الذي يمكن إعادة كتابته، يمكن أن يشير إلى أي نوع وحجم من القنوات، أو أي تنفيذ آخر لـ AbstractChannel
.
المنشئ RemoteChannel(f::Function, pid)()
يتيح لنا إنشاء مراجع لقنوات تحمل أكثر من قيمة من نوع محدد. f
هو دالة تُنفذ على pid
ويجب أن تُرجع AbstractChannel
.
على سبيل المثال، RemoteChannel(()->Channel{Int}(10), pid)
، ستعيد مرجعًا إلى قناة من النوع Int
وحجم 10. القناة موجودة على العامل pid
.
طرق put!
، take!
، fetch
، isready
و wait
على RemoteChannel
يتم توجيهها إلى مخزن البيانات في العملية البعيدة.
RemoteChannel
يمكن استخدامه للإشارة إلى كائنات AbstractChannel
التي تم تنفيذها بواسطة المستخدم. مثال بسيط على ذلك هو DictChannel
الذي يستخدم قاموسًا كمتجره البعيد:
julia> struct DictChannel{T} <: AbstractChannel{T}
d::Dict
cond_take::Threads.Condition # waiting for data to become available
DictChannel{T}() where {T} = new(Dict(), Threads.Condition())
DictChannel() = DictChannel{Any}()
end
julia> begin
function Base.put!(D::DictChannel, k, v)
@lock D.cond_take begin
D.d[k] = v
notify(D.cond_take)
end
return D
end
function Base.take!(D::DictChannel, k)
@lock D.cond_take begin
v = fetch(D, k)
delete!(D.d, k)
return v
end
end
Base.isready(D::DictChannel) = @lock D.cond_take !isempty(D.d)
Base.isready(D::DictChannel, k) = @lock D.cond_take haskey(D.d, k)
function Base.fetch(D::DictChannel, k)
@lock D.cond_take begin
wait(D, k)
return D.d[k]
end
end
function Base.wait(D::DictChannel, k)
@lock D.cond_take begin
while !isready(D, k)
wait(D.cond_take)
end
end
end
end;
julia> d = DictChannel();
julia> isready(d)
false
julia> put!(d, :k, :v);
julia> isready(d, :k)
true
julia> fetch(d, :k)
:v
julia> wait(d, :k)
julia> take!(d, :k)
:v
julia> isready(d, :k)
false
Channels and RemoteChannels
- A
Channel
محلي لعملية. لا يمكن للعامل 2 الإشارة مباشرة إلى4d61726b646f776e2e436f64652822222c20224368616e6e656c2229_40726566
على العامل 3 والعكس صحيح. ومع ذلك، يمكن لـRemoteChannel
وضع وسحب القيم عبر العمال. - A
RemoteChannel
يمكن اعتباره بمثابة مقبض لـChannel
. - معرف رقم العملية،
pid
، المرتبط بـRemoteChannel
يحدد العملية التي يوجد فيها التخزين الاحتياطي، أي، التخزين الاحتياطيChannel
. - أي عملية تشير إلى
RemoteChannel
يمكنها إضافة وإزالة العناصر من القناة. يتم إرسال البيانات تلقائيًا إلى (أو استردادها من) العملية المرتبطة بـ4d61726b646f776e2e436f64652822222c202252656d6f74654368616e6e656c2229_40726566
. - تسلسل
Channel
يقوم أيضًا بتسلسل أي بيانات موجودة في القناة. وبالتالي، فإن فك تسلسله يجعل نسخة من الكائن الأصلي. - من ناحية أخرى، تتضمن عملية تسلسل
RemoteChannel
فقط تسلسل معرف يحدد الموقع والحالة لـChannel
المشار إليه بواسطة المقبض. وبالتالي، فإن كائن4d61726b646f776e2e436f64652822222c202252656d6f74654368616e6e656c2229_40726566
الذي تم إلغاء تسلسله (على أي عامل) يشير أيضًا إلى نفس مخزن البيانات مثل الأصل.
يمكن تعديل مثال القنوات من أعلاه للتواصل بين العمليات، كما هو موضح أدناه.
نبدأ 4 عمال لمعالجة قناة jobs
عن بُعد. يتم كتابة الوظائف، التي يتم التعرف عليها بواسطة معرف (job_id
)، إلى القناة. كل مهمة يتم تنفيذها عن بُعد في هذه المحاكاة تقرأ job_id
، تنتظر لفترة عشوائية من الوقت وتكتب مرة أخرى مجموعة من job_id
، الوقت المستغرق ومعرف العملية الخاصة بها (pid
) إلى قناة النتائج. أخيرًا، يتم طباعة جميع results
في عملية الماستر.
julia> addprocs(4); # add worker processes
julia> const jobs = RemoteChannel(()->Channel{Int}(32));
julia> const results = RemoteChannel(()->Channel{Tuple}(32));
julia> @everywhere function do_work(jobs, results) # define work function everywhere
while true
job_id = take!(jobs)
exec_time = rand()
sleep(exec_time) # simulates elapsed time doing actual work
put!(results, (job_id, exec_time, myid()))
end
end
julia> function make_jobs(n)
for i in 1:n
put!(jobs, i)
end
end;
julia> n = 12;
julia> errormonitor(@async make_jobs(n)); # feed the jobs channel with "n" jobs
julia> for p in workers() # start tasks on the workers to process requests in parallel
remote_do(do_work, p, jobs, results)
end
julia> @elapsed while n > 0 # print out results
job_id, exec_time, where = take!(results)
println("$job_id finished in $(round(exec_time; digits=2)) seconds on worker $where")
global n = n - 1
end
1 finished in 0.18 seconds on worker 4
2 finished in 0.26 seconds on worker 5
6 finished in 0.12 seconds on worker 4
7 finished in 0.18 seconds on worker 4
5 finished in 0.35 seconds on worker 5
4 finished in 0.68 seconds on worker 2
3 finished in 0.73 seconds on worker 3
11 finished in 0.01 seconds on worker 3
12 finished in 0.02 seconds on worker 3
9 finished in 0.26 seconds on worker 5
8 finished in 0.57 seconds on worker 4
10 finished in 0.58 seconds on worker 2
0.055971741
Remote References and Distributed Garbage Collection
يمكن تحرير الكائنات المشار إليها بواسطة المراجع البعيدة فقط عندما يتم حذف جميع المراجع المحتفظ بها في الكتلة.
العقدة التي يتم تخزين القيمة فيها تتعقب أي من العمال لديه مرجع إليها. في كل مرة يتم فيها تسلسل RemoteChannel
أو Future
(غير مسترجع) إلى عامل، يتم إبلاغ العقدة المشار إليها بواسطة المرجع. وفي كل مرة يتم فيها جمع القمامة محليًا لـ 4d61726b646f776e2e436f64652822222c202252656d6f74654368616e6e656c2229_40726566
أو 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
(غير مسترجع)، يتم إبلاغ العقدة التي تمتلك القيمة مرة أخرى. يتم تنفيذ ذلك في مسلسِل واعٍ داخليًا للمجموعة. المراجع البعيدة صالحة فقط في سياق مجموعة قيد التشغيل. تسلسل وفك تسلسل المراجع إلى ومن كائنات IO
العادية غير مدعوم.
تتم الإشعارات من خلال إرسال رسائل "التتبع" - رسالة "إضافة مرجع" عندما يتم تسلسل مرجع إلى عملية مختلفة ورسالة "حذف مرجع" عندما يتم جمع المرجع محليًا.
نظرًا لأن Future
هي كتابة مرة واحدة ومخزنة محليًا، فإن فعل fetch
لـ 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
يقوم أيضًا بتحديث معلومات تتبع المرجع على العقدة التي تمتلك القيمة.
العقدة التي تمتلك القيمة تحررها بمجرد أن يتم مسح جميع الإشارات إليها.
مع Future
، فإن تسلسل 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
الذي تم جلبه بالفعل إلى عقدة مختلفة يرسل أيضًا القيمة نظرًا لأن المتجر البعيد الأصلي قد يكون قد جمع القيمة في هذا الوقت.
من المهم أن نلاحظ أن متى يتم جمع القمامة محليًا لكائن ما يعتمد على حجم الكائن والضغط الحالي على الذاكرة في النظام.
في حالة المراجع البعيدة، يكون حجم كائن المرجع المحلي صغيرًا جدًا، بينما قد تكون القيمة المخزنة على العقدة البعيدة كبيرة جدًا. نظرًا لأن الكائن المحلي قد لا يتم جمعه على الفور، فمن الجيد ممارسة استدعاء finalize
على النسخ المحلية من RemoteChannel
، أو على Future
غير المسترجعة. نظرًا لأن استدعاء fetch
على 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
يزيل أيضًا مرجعه من المتجر البعيد، فلا حاجة لذلك على 4d61726b646f776e2e436f64652822222c20224675747572652229_407265662044697374726962757465642e467574757265
المسترجعة. يؤدي استدعاء 4d61726b646f776e2e436f64652822222c202266696e616c697a652229_40726566
بشكل صريح إلى إرسال رسالة فورية إلى العقدة البعيدة للذهاب قدمًا وإزالة مرجعها للقيمة.
بمجرد الانتهاء، يصبح المرجع غير صالح ولا يمكن استخدامه في أي مكالمات أخرى.
Local invocations
يتم نسخ البيانات بالضرورة إلى العقدة البعيدة للتنفيذ. هذه هي الحالة لكل من المكالمات البعيدة وعندما يتم تخزين البيانات في RemoteChannel
/ Future
على عقدة مختلفة. كما هو متوقع، فإن هذا يؤدي إلى نسخة من الكائنات المسلسلة على العقدة البعيدة. ومع ذلك، عندما تكون العقدة الوجهة هي العقدة المحلية، أي أن معرف عملية الاستدعاء هو نفسه معرف العقدة البعيدة، يتم تنفيذها كمكالمة محلية. عادةً (ليس دائمًا) يتم تنفيذها في مهمة مختلفة - ولكن لا يوجد تسلسل/فك تسلسل للبيانات. وبالتالي، تشير المكالمة إلى نفس مثيلات الكائنات كما تم تمريرها - لا يتم إنشاء نسخ. يتم تسليط الضوء على هذا السلوك أدناه:
julia> using Distributed;
julia> rc = RemoteChannel(()->Channel(3)); # RemoteChannel created on local node
julia> v = [0];
julia> for i in 1:3
v[1] = i # Reusing `v`
put!(rc, v)
end;
julia> result = [take!(rc) for _ in 1:3];
julia> println(result);
Array{Int64,1}[[3], [3], [3]]
julia> println("Num Unique objects : ", length(unique(map(objectid, result))));
Num Unique objects : 1
julia> addprocs(1);
julia> rc = RemoteChannel(()->Channel(3), workers()[1]); # RemoteChannel created on remote node
julia> v = [0];
julia> for i in 1:3
v[1] = i
put!(rc, v)
end;
julia> result = [take!(rc) for _ in 1:3];
julia> println(result);
Array{Int64,1}[[1], [2], [3]]
julia> println("Num Unique objects : ", length(unique(map(objectid, result))));
Num Unique objects : 3
كما يتضح، put!
على RemoteChannel
المملوكة محليًا مع نفس الكائن v
المعدل بين الاستدعاءات ينتج عنه نفس مثيل الكائن الفردي المخزن. على عكس نسخ v
التي يتم إنشاؤها عندما يكون العقدة المالكة لـ rc
هي عقدة مختلفة.
من المهم ملاحظة أن هذه ليست عادةً مشكلة. إنها شيء يجب أخذه في الاعتبار فقط إذا كان الكائن يتم تخزينه محليًا وتعديله بعد الاستدعاء. في مثل هذه الحالات، قد يكون من المناسب تخزين نسخة عميقة (deepcopy
) من الكائن.
هذا صحيح أيضًا بالنسبة للمكالمات البعيدة على العقدة المحلية كما هو موضح في المثال التالي:
julia> using Distributed; addprocs(1);
julia> v = [0];
julia> v2 = remotecall_fetch(x->(x[1] = 1; x), myid(), v); # Executed on local node
julia> println("v=$v, v2=$v2, ", v === v2);
v=[1], v2=[1], true
julia> v = [0];
julia> v2 = remotecall_fetch(x->(x[1] = 1; x), workers()[1], v); # Executed on remote node
julia> println("v=$v, v2=$v2, ", v === v2);
v=[0], v2=[1], false
كما يمكن رؤيته مرة أخرى، فإن استدعاء بعيد على العقدة المحلية يتصرف تمامًا مثل الاستدعاء المباشر. يقوم الاستدعاء بتعديل الكائنات المحلية الممررة كوسائط. في الاستدعاء البعيد، يعمل على نسخة من الوسائط.
لتكرار، بشكل عام هذه ليست مشكلة. إذا كان العقدة المحلية تُستخدم أيضًا كعقدة حسابية، ويجب أخذ السلوك في الاعتبار عند استخدام المعاملات بعد الاستدعاء، وإذا لزم الأمر يجب تمرير نسخ عميقة من المعاملات إلى الاستدعاء الذي تم استدعاؤه على العقدة المحلية. ستعمل الاستدعاءات على العقد البعيدة دائمًا على نسخ من المعاملات.
Shared Arrays
تستخدم المصفوفات المشتركة الذاكرة المشتركة للنظام لخرائط نفس المصفوفة عبر العديد من العمليات. إن SharedArray
هو خيار جيد عندما تريد أن يكون لديك كمية كبيرة من البيانات يمكن الوصول إليها بشكل مشترك من قبل عمليتين أو أكثر على نفس الجهاز. دعم المصفوفات المشتركة متاح عبر الوحدة SharedArrays
، والتي يجب تحميلها صراحةً على جميع العمال المشاركين.
يوفر هيكل بيانات تكميلي من خلال الحزمة الخارجية DistributedArrays.jl
في شكل DArray
. بينما توجد بعض أوجه التشابه مع SharedArray
، فإن سلوك DArray
مختلف تمامًا. في 4d61726b646f776e2e436f64652822222c202253686172656441727261792229_40726566
، يتمتع كل "عملية" مشاركة بالوصول إلى المصفوفة بالكامل؛ في المقابل، في 4d61726b646f776e2e436f64652822222c20224441727261792229_68747470733a2f2f6769746875622e636f6d2f4a756c6961506172616c6c656c2f44697374726962757465644172726179732e6a6c
، يتمتع كل عملية بالوصول المحلي إلى جزء فقط من البيانات، ولا تشارك عمليتان نفس الجزء.
SharedArray
يعمل الفهرسة (تعيين والوصول إلى القيم) تمامًا كما هو الحال مع المصفوفات العادية، وهو فعال لأن الذاكرة الأساسية متاحة للعملية المحلية. لذلك، تعمل معظم الخوارزميات بشكل طبيعي على 4d61726b646f776e2e436f64652822222c202253686172656441727261792229_40726566
s، على الرغم من أنها في وضع عملية واحدة. في الحالات التي تصر فيها خوارزمية على إدخال Array
، يمكن استرداد المصفوفة الأساسية من 4d61726b646f776e2e436f64652822222c202253686172656441727261792229_40726566
عن طريق استدعاء sdata
. بالنسبة لأنواع AbstractArray
الأخرى، فإن 4d61726b646f776e2e436f64652822222c202273646174612229_40726566
تعيد الكائن نفسه، لذا من الآمن استخدام 4d61726b646f776e2e436f64652822222c202273646174612229_40726566
على أي كائن من نوع Array
.
الباني لمصفوفة مشتركة يكون على الشكل التالي:
SharedArray{T,N}(dims::NTuple; init=false, pids=Int[])
الذي ينشئ مصفوفة مشتركة من نوع T
بحجم dims
بعدد أبعاد N
عبر العمليات المحددة بواسطة pids
. على عكس المصفوفات الموزعة، فإن المصفوفة المشتركة يمكن الوصول إليها فقط من قبل العمال المشاركين المحددين بواسطة وسيط الاسم pids
(وعملية الإنشاء أيضًا، إذا كانت على نفس المضيف). لاحظ أن العناصر المدعومة في SharedArray هي فقط isbits
.
إذا تم تحديد دالة 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)] = repeat([myid()], length(localindices(S))))
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)] = repeat([myid()], length( indexpids(S):length(procs(S)):length(S))))
3×4 SharedArray{Int64,2}:
2 2 2 2
3 3 3 3
4 4 4 4
نظرًا لأن جميع العمليات لديها وصول إلى البيانات الأساسية، يجب أن تكون حذرًا في عدم إنشاء تعارضات. على سبيل المثال:
@sync begin
for p in procs(S)
@async begin
remotecall_wait(fill!, p, S, p)
end
end
end
سيؤدي ذلك إلى سلوك غير محدد. لأن كل عملية تملأ كامل المصفوفة بمعرف العملية الخاص بها pid
، فإن أي عملية تكون الأخيرة في التنفيذ (لأي عنصر معين من S
) ستحتفظ بمعرف العملية الخاص بها.
كمثال أكثر تعقيدًا وطولًا، اعتبر تشغيل "النواة" التالية بالتوازي:
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
s وقمنا بتوقيت هذه الدوال، نحصل على النتائج التالية (مع 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!
هي أنها تقلل من حركة المرور بين العمال، مما يسمح لكل منهم بالحساب لفترة ممتدة على الجزء المعين.
Shared Arrays and Distributed Garbage Collection
مثل المراجع البعيدة، تعتمد المصفوفات المشتركة أيضًا على جمع القمامة في العقدة المنشئة لتحرير المراجع من جميع العمال المشاركين. سيستفيد الكود الذي ينشئ العديد من كائنات المصفوفة المشتركة قصيرة العمر من إنهاء هذه الكائنات بشكل صريح في أقرب وقت ممكن. وهذا يؤدي إلى تحرير كل من الذاكرة ومقابض الملفات التي تقوم بتعيين الجزء المشترك في وقت أقرب.
ClusterManagers
إطلاق وإدارة وتوصيل عمليات جوليا في مجموعة منطقية يتم عبر مديري المجموعات. يتحمل ClusterManager
مسؤولية
- إطلاق عمليات العامل في بيئة العنقود
- إدارة الأحداث خلال فترة حياة كل عامل
- اختياريًا، توفير نقل البيانات
تتميز مجموعة جوليا بالخصائص التالية:
- العملية الأولية لجوليا، والتي تُسمى أيضًا
master
، هي خاصة ولهاid
يساوي 1. - يمكن فقط لعملية
master
إضافة أو إزالة عمليات العامل. - يمكن لجميع العمليات التواصل مباشرة مع بعضها البعض.
يتم إنشاء الاتصالات بين العمال (باستخدام بروتوكول TCP/IP المدمج) بالطريقة التالية:
addprocs
يتم استدعاؤه على العملية الرئيسية مع كائنClusterManager
.addprocs
يستدعي الطريقة المناسبةlaunch
التي تقوم بإنشاء العدد المطلوب من عمليات العمل على الآلات المناسبة.- يبدأ كل عامل بالاستماع على منفذ مجاني ويكتب معلومات المضيف والمنفذ الخاصة به إلى
stdout
. - يدير مدير الكتلة
stdout
لكل عامل ويجعله متاحًا لعملية الماستر. - يقوم عملية الماستر بتحليل هذه المعلومات وإعداد اتصالات TCP/IP مع كل عامل.
- يتم إبلاغ كل عامل أيضًا عن العمال الآخرين في الكتلة.
- يتصل كل عامل بجميع العمال الذين يكون
id
الخاص بهم أقل منid
الخاص بالعامل نفسه. - بهذه الطريقة يتم إنشاء شبكة شبكية، حيث يكون كل عامل متصلاً مباشرة بكل عامل آخر.
بينما يستخدم طبقة النقل الافتراضية TCPSocket
، من الممكن أن يوفر تجمع جوليا وسيلة النقل الخاصة به.
تقدم جوليا مديري مجموعات مدمجين:
LocalManager
، يُستخدم عندما يتم استدعاءaddprocs()
أوaddprocs(np::Integer)
SSHManager
، يُستخدم عندما يتم استدعاءaddprocs(hostnames::Array)
مع قائمة من أسماء المضيفين
LocalManager
يُستخدم لإطلاق عمال إضافيين على نفس المضيف، مما يستفيد من الأجهزة متعددة النوى والمعالجات.
لذا، سيحتاج مدير الكتلة الحد الأدنى إلى:
- كن نوعًا فرعيًا من
ClusterManager
المجرد - تنفيذ
launch
، وهي طريقة مسؤولة عن إطلاق عمال جدد - تنفيذ
manage
، والذي يتم استدعاؤه في أحداث مختلفة خلال حياة العامل (على سبيل المثال، إرسال إشارة مقاطعة)
addprocs(manager::FooManager)
يتطلب من FooManager
تنفيذ:
function launch(manager::FooManager, params::Dict, launched::Array, c::Condition)
[...]
end
function manage(manager::FooManager, id::Integer, config::WorkerConfig, op::Symbol)
[...]
end
كمثال، دعنا نرى كيف يتم تنفيذ LocalManager
، المدير المسؤول عن بدء العمال على نفس المضيف:
struct LocalManager <: ClusterManager
np::Integer
end
function launch(manager::LocalManager, params::Dict, launched::Array, c::Condition)
[...]
end
function manage(manager::LocalManager, id::Integer, config::WorkerConfig, op::Symbol)
[...]
end
تأخذ طريقة launch
الوسائط التالية:
manager::ClusterManager
: مدير الكلاستر الذي يتم استدعاؤه معaddprocs
params::Dict
: جميع الوسائط الرئيسية المرسلة إلىaddprocs
launched::Array
: المصفوفة لإضافة واحد أو أكثر من كائناتWorkerConfig
إليهاc::Condition
: متغير الحالة الذي سيتم إخطاره عند إطلاق العمال
تتم دعوة طريقة launch
بشكل غير متزامن في مهمة منفصلة. تشير نهاية هذه المهمة إلى أن جميع العمال المطلوبين قد تم إطلاقهم. لذلك، يجب أن تخرج دالة 4d61726b646f776e2e436f64652822222c20226c61756e63682229_40726566
بمجرد أن يتم إطلاق جميع العمال المطلوبين.
يتم ربط العمال الذين تم إطلاقهم حديثًا ببعضهم البعض وبعملية الماستر بطريقة شاملة. يؤدي تحديد وسيط سطر الأوامر --worker[=<cookie>]
إلى قيام العمليات التي تم إطلاقها بتهيئة نفسها كعمال وإعداد الاتصالات عبر مآخذ TCP/IP.
يتشارك جميع العمال في مجموعة نفس cookie مثل الرئيسي. عندما لا يتم تحديد الكوكي، أي مع خيار --worker
، يحاول العامل قراءته من مدخلاته القياسية. يقوم كل من LocalManager
و SSHManager
بتمرير الكوكي إلى العمال الجدد الذين تم إطلاقهم عبر مدخلاتهم القياسية.
بشكل افتراضي، سيستمع العامل على منفذ مجاني على العنوان الذي تم إرجاعه بواسطة استدعاء getipaddr()
. يمكن تحديد عنوان معين للاستماع عليه بواسطة الوسيطة الاختيارية --bind-to bind_addr[:port]
. هذا مفيد للمضيفين ذوي العناوين المتعددة.
كمثال على وسيلة نقل غير TCP/IP، قد تختار التنفيذ استخدام MPI، وفي هذه الحالة يجب عدم تحديد --worker
. بدلاً من ذلك، يجب على العمال الذين تم إطلاقهم حديثًا استدعاء init_worker(cookie)
قبل استخدام أي من البنى المتوازية.
لكل عامل يتم إطلاقه، يجب أن تضيف طريقة launch
كائن WorkerConfig
(مع تهيئة الحقول المناسبة) إلى launched
mutable struct WorkerConfig
# Common fields relevant to all cluster managers
io::Union{IO, Nothing}
host::Union{AbstractString, Nothing}
port::Union{Integer, Nothing}
# Used when launching additional workers at a host
count::Union{Int, Symbol, Nothing}
exename::Union{AbstractString, Cmd, Nothing}
exeflags::Union{Cmd, Nothing}
# External cluster managers can use this to store information at a per-worker level
# Can be a dict if multiple fields need to be stored.
userdata::Any
# SSHManager / SSH tunnel connections to workers
tunnel::Union{Bool, Nothing}
bind_addr::Union{AbstractString, Nothing}
sshflags::Union{Cmd, Nothing}
max_parallel::Union{Integer, Nothing}
# Used by Local/SSH managers
connect_at::Any
[...]
end
تُستخدم معظم الحقول في WorkerConfig
بواسطة المديرين المدمجين. عادةً ما تحدد مدراء الكتل المخصصون فقط io
أو host
/ port
:
إذا تم تحديد
io
، فإنه يُستخدم لقراءة معلومات المضيف/المنفذ. يقوم عامل جوليا بطباعة عنوان الربط والمنفذ الخاص به عند بدء التشغيل. وهذا يسمح لعمال جوليا بالاستماع على أي منفذ مجاني متاح بدلاً من الحاجة إلى تكوين منافذ العمال يدويًا.إذا لم يتم تحديد
io
، يتم استخدامhost
وport
للاتصال.count
وexename
وexeflags
ذات صلة بإطلاق عمال إضافيين من عامل. على سبيل المثال، قد يقوم مدير الكتلة بإطلاق عامل واحد لكل عقدة، واستخدام ذلك لإطلاق عمال إضافيين.count
مع قيمة صحيحةn
ستطلق إجماليn
من العمال.count
بقيمة:auto
سيطلق عددًا من العمال يساوي عدد خيوط وحدة المعالجة المركزية (النوى المنطقية) على تلك الآلة.exename
هو اسم تنفيذjulia
بما في ذلك المسار الكامل.exeflags
يجب أن يتم تعيينه إلى الوسائط المطلوبة لخط الأوامر للعمال الجدد.
tunnel
وbind_addr
وsshflags
وmax_parallel
تُستخدم عند الحاجة إلى نفق ssh للاتصال بالعمال من عملية الماستر.userdata
متاحة لمديري الكتل المخصصين لتخزين معلومات خاصة بالعمال الخاصة بهم.
manage(manager::FooManager, id::Integer, config::WorkerConfig, op::Symbol)
يتم استدعاؤه في أوقات مختلفة خلال حياة العامل مع قيم op
المناسبة:
- مع
:register
/:deregister
عند إضافة عامل / إزالة عامل من مجموعة عمال جوليا. - مع
:interrupt
عندما يتم استدعاءinterrupt(workers)
. يجب علىClusterManager
إرسال إشارة مقاطعة إلى العامل المناسب. - مع
:finalize
لأغراض التنظيف.
Cluster Managers with Custom Transports
استبدال اتصالات المقابس الافتراضية TCP/IP من كل إلى كل بطبقة نقل مخصصة هو أمر أكثر تعقيدًا قليلاً. يحتوي كل عملية جوليا على عدد من مهام الاتصال بقدر عدد العمال المتصل بهم. على سبيل المثال، اعتبر مجموعة جوليا مكونة من 32 عملية في شبكة مصفوفة من كل إلى كل:
- كل عملية جوليا لديها 31 مهمة تواصل.
- يتعامل كل مهمة مع جميع الرسائل الواردة من عامل بعيد واحد في حلقة معالجة الرسائل.
- تنتظر حلقة معالجة الرسائل على كائن
IO
(على سبيل المثال،TCPSocket
في التنفيذ الافتراضي)، تقرأ رسالة كاملة، تعالجها وتنتظر الرسالة التالية. - إرسال الرسائل إلى عملية يتم مباشرة من أي مهمة في جوليا - ليس فقط مهام الاتصال - مرة أخرى، عبر كائن
IO
المناسب.
يتطلب استبدال وسيلة النقل الافتراضية من التنفيذ الجديد إعداد اتصالات مع العمال البعيدين وتوفير كائنات IO
المناسبة التي يمكن أن تنتظر عليها حلقات معالجة الرسائل. يجب تنفيذ ردود الفعل الخاصة بالمدير وهي:
connect(manager::FooManager, pid::Integer, config::WorkerConfig)
kill(manager::FooManager, pid::Int, config::WorkerConfig)
التنفيذ الافتراضي (الذي يستخدم مآخذ TCP/IP) يتم تنفيذه كـ connect(manager::ClusterManager, pid::Integer, config::WorkerConfig)
.
connect
يجب أن يُرجع زوجًا من كائنات IO
، واحد لقراءة البيانات المرسلة من العامل pid
، والآخر لكتابة البيانات التي تحتاج إلى أن تُرسل إلى العامل pid
. يمكن لمديري المجموعات المخصصين استخدام BufferStream
في الذاكرة كوسيلة لنقل البيانات بين النقل المخصص، الذي قد لا يكون IO
، والبنية التحتية الموازية المدمجة في جوليا.
A BufferStream
هو IOBuffer
في الذاكرة والذي يتصرف مثل IO
– إنه تدفق يمكن التعامل معه بشكل غير متزامن.
المجلد clustermanager/0mq
في Examples repository يحتوي على مثال لاستخدام ZeroMQ لربط عمال جوليا في طوبولوجيا نجمية مع وسيط 0MQ في المنتصف. ملاحظة: عمليات جوليا لا تزال جميعها منطقياً متصلة ببعضها البعض - يمكن لأي عامل إرسال رسالة إلى أي عامل آخر مباشرة دون أي وعي باستخدام 0MQ كطبقة نقل.
عند استخدام وسائل النقل المخصصة:
- يجب ألا يتم بدء عمال جوليا باستخدام
--worker
. سيؤدي البدء باستخدام--worker
إلى افتراض أن العمال الذين تم إطلاقهم حديثًا يستخدمون تنفيذ نقل مقبس TCP/IP. - لكل اتصال منطقي وارد مع عامل، يجب استدعاء
Base.process_messages(rd::IO, wr::IO)()
. هذا يطلق مهمة جديدة تتعامل مع قراءة وكتابة الرسائل من/إلى العامل الممثل بواسطة كائناتIO
. init_worker(cookie, manager::FooManager)
يجب أن يتم استدعاؤه كجزء من تهيئة عملية العامل.- يمكن تعيين الحقل
connect_at::Any
فيWorkerConfig
بواسطة مدير الكتلة عندما يتم استدعاءlaunch
. يتم تمرير قيمة هذا الحقل في جميع ردودconnect
. عادةً، يحمل معلومات حول كيفية الاتصال بالعامل. على سبيل المثال، يستخدم نقل مقبس TCP/IP هذا الحقل لتحديد مجموعة(host, port)
التي يجب الاتصال بها بالعامل.
kill(manager, pid, config)
يتم استدعاؤه لإزالة عامل من الكتلة. يجب على عملية الماستر إغلاق كائنات IO
المقابلة بواسطة التنفيذ لضمان التنظيف السليم. يقوم التنفيذ الافتراضي ببساطة بتنفيذ استدعاء exit()
على العامل البعيد المحدد.
مجلد الأمثلة clustermanager/simple
هو مثال يوضح تنفيذًا بسيطًا باستخدام مقابس نطاق UNIX لإعداد الكلاستر.
Network Requirements for LocalManager and SSHManager
تم تصميم مجموعات جوليا ليتم تنفيذها في بيئات مؤمنة مسبقًا على بنية تحتية مثل أجهزة الكمبيوتر المحمولة المحلية، أو المجموعات القسيمية، أو حتى السحابة. تغطي هذه القسم متطلبات أمان الشبكة لـ LocalManager
و SSHManager
المدمجين:
لا يستمع عملية الماستر على أي منفذ. إنها تتصل فقط بالعمال.
كل عامل يرتبط بواحدة فقط من الواجهات المحلية ويستمع على رقم منفذ عابر يتم تعيينه بواسطة نظام التشغيل.
LocalManager
، الذي يستخدمهaddprocs(N)
، يقوم افتراضيًا بالربط فقط بواجهة الحلقة المحلية. وهذا يعني أن العمال الذين يتم تشغيلهم لاحقًا على مضيفين بعيدين (أو من قبل أي شخص لديه نوايا خبيثة) غير قادرين على الاتصال بالعنقود. ستفشل عمليةaddprocs(4)
تليهاaddprocs(["remote_host"])
. قد يحتاج بعض المستخدمين إلى إنشاء عنقود يتكون من نظامهم المحلي وعدد قليل من الأنظمة البعيدة. يمكن القيام بذلك من خلال طلبLocalManager
صراحةً للربط بواجهة شبكة خارجية عبر وسيطrestrict
:addprocs(4; restrict=false)
.SSHManager
، الذي يستخدمهaddprocs(list_of_remote_hosts)
، يقوم بإطلاق العمال على المضيفين البعيدين عبر SSH. بشكل افتراضي، يتم استخدام SSH فقط لإطلاق عمال جوليا. تستخدم الاتصالات اللاحقة بين الماستر والعمال والعمال فيما بينهم مآخذ TCP/IP عادية وغير مشفرة. يجب أن يكون تسجيل الدخول بدون كلمة مرور مفعلًا على المضيفين البعيدين. يمكن تحديد أعلام SSH إضافية أو بيانات اعتماد عبر وسيط الكلمةsshflags
.addprocs(list_of_remote_hosts; tunnel=true, sshflags=<ssh keys and other flags>)
مفيد عندما نرغب في استخدام اتصالات SSH للماستر-عامل أيضًا. سيناريو نموذجي لذلك هو لابتوب محلي يعمل على REPL جوليا (أي، الماستر) مع بقية الكلاستر على السحابة، لنقل على أمازون EC2. في هذه الحالة، يحتاج فقط المنفذ 22 إلى أن يكون مفتوحًا في الكلاستر البعيد مع عميل SSH مصادق عليه عبر بنية المفتاح العام (PKI). يمكن تزويد بيانات اعتماد المصادقة عبرsshflags
، على سبيل المثالsshflags=`-i <keyfile>`
.في بنية الشبكة الشاملة (الافتراضية)، تتصل جميع العمال ببعضهم البعض عبر مآخذ TCP العادية. لذلك، يجب أن تضمن سياسة الأمان على عقد الكتلة الاتصال الحر بين العمال لنطاق المنافذ المؤقتة (يختلف حسب نظام التشغيل).
تأمين وتشفير جميع حركة المرور بين العمال (عبر SSH) أو تشفير الرسائل الفردية يمكن أن يتم من خلال
ClusterManager
مخصص.إذا قمت بتحديد
multiplex=true
كخيار لـaddprocs
، يتم استخدام تعدد الإرسال لإنشاء نفق بين الماستر والعمال. إذا كنت قد قمت بتكوين تعدد الإرسال بنفسك وتم إنشاء الاتصال بالفعل، يتم استخدام تعدد الإرسال بغض النظر عن خيارmultiplex
. إذا كان تعدد الإرسال مفعلًا، يتم تعيين التوجيه باستخدام الاتصال الحالي (-O forward
خيار في ssh). هذا مفيد إذا كانت خوادمك تتطلب مصادقة كلمة المرور؛ يمكنك تجنب المصادقة في جوليا عن طريق تسجيل الدخول إلى الخادم قبل4d61726b646f776e2e436f64652822222c202261646470726f63732229_40726566
. سيتم وضع مقبس التحكم في~/.ssh/julia-%r@%h:%p
خلال الجلسة ما لم يتم استخدام اتصال تعدد الإرسال الحالي. لاحظ أن عرض النطاق قد يكون محدودًا إذا قمت بإنشاء عمليات متعددة على عقدة وتم تفعيل تعدد الإرسال، لأنه في هذه الحالة تشارك العمليات اتصال TCP واحد لتعدد الإرسال.
Cluster Cookie
تشارك جميع العمليات في العنقود نفس الكوكي الذي، بشكل افتراضي، هو سلسلة تم إنشاؤها عشوائيًا في عملية الماستر:
cluster_cookie()
يعيد الكوكي، بينماcluster_cookie(cookie)()
يقوم بتعيينه ويعيد الكوكي الجديد.- جميع الاتصالات مصدقة من الجانبين لضمان أن العمال الذين بدأهم الماستر فقط هم المسموح لهم بالاتصال ببعضهم البعض.
- يمكن تمرير الكوكي إلى العمال عند بدء التشغيل عبر الوسيطة
--worker=<cookie>
. إذا تم تحديد الوسيطة--worker
بدون الكوكي، يحاول العامل قراءة الكوكي من مدخلاته القياسية (stdin
). يتم إغلاقstdin
على الفور بعد استرجاع الكوكي. - يمكن لـ
ClusterManager
استرداد الكوكي من الماستر عن طريق استدعاءcluster_cookie()
. يجب على مديري الكلاستر الذين لا يستخدمون النقل الافتراضي TCP/IP (وبالتالي لا يحددون--worker
) استدعاءinit_worker(cookie, manager)
بنفس الكوكي كما هو موجود على الماستر.
لاحظ أن البيئات التي تتطلب مستويات أعلى من الأمان يمكنها تنفيذ ذلك من خلال ClusterManager
مخصص. على سبيل المثال، يمكن مشاركة الكوكيز مسبقًا وبالتالي لا يتم تحديدها كحجة بدء تشغيل.
Specifying Network Topology (Experimental)
تُستخدم حجة الكلمة الرئيسية topology
الممررة إلى addprocs
لتحديد كيفية اتصال العمال ببعضهم البعض:
:all_to_all
، الافتراضي: جميع العمال متصلون ببعضهم البعض.:master_worker
: فقط عملية السائق، أيpid
1، لديها اتصالات بالعمال.:custom
: تحدد طريقةlaunch
لمدير الكتلة topology الاتصال عبر الحقولident
وconnect_idents
فيWorkerConfig
. سيتصل عامل له هوية مقدمة من مدير الكتلةident
بجميع العمال المحددين فيconnect_idents
.
تؤثر حجة الكلمة الرئيسية lazy=true|false
فقط على خيار topology
:all_to_all
. إذا كانت true
، يبدأ العنقود مع الاتصال بين الماستر وجميع العمال. يتم إنشاء اتصالات العمال-العمال المحددة عند أول استدعاء عن بُعد بين عاملين. يساعد ذلك في تقليل الموارد الأولية المخصصة للتواصل داخل العنقود. يتم إعداد الاتصالات اعتمادًا على متطلبات وقت التشغيل لبرنامج متوازي. القيمة الافتراضية لـ lazy
هي true
.
حاليًا، يؤدي إرسال رسالة بين العمال غير المتصلين إلى حدوث خطأ. يجب اعتبار هذا السلوك، كما هو الحال مع الوظائف والواجهة، تجريبيًا بطبيعته وقد يتغير في الإصدارات المستقبلية.
Noteworthy external packages
بجانب التوازي في جوليا، هناك العديد من الحزم الخارجية التي يجب ذكرها. على سبيل المثال، MPI.jl
هو غلاف جوليا لبروتوكول MPI
، وDagger.jl
يوفر وظائف مشابهة لـ Dask في بايثون، وDistributedArrays.jl
يوفر عمليات مصفوفات موزعة عبر العمال، كما هو الحال مع outlined above.
يجب الإشارة إلى نظام برمجة GPU الخاص بـ Julia، والذي يتضمن:
- CUDA.jl يلتف حول مكتبات CUDA المختلفة ويدعم تجميع نوى جوليا لوحدات معالجة الرسوميات Nvidia.
- oneAPI.jl يلتف حول نموذج البرمجة الموحد oneAPI، ويدعم تنفيذ نوى جوليا على المسرعات المدعومة. حاليًا، يتم دعم نظام لينكس فقط.
- AMDGPU.jl يلتف حول مكتبات AMD ROCm ويدعم تجميع نوى جوليا لبطاقات AMD الرسومية. حاليًا، يتم دعم نظام لينكس فقط.
- مكتبات عالية المستوى مثل KernelAbstractions.jl، Tullio.jl و ArrayFire.jl.
في المثال التالي سنستخدم كل من DistributedArrays.jl
و CUDA.jl
لتوزيع مصفوفة عبر عمليات متعددة من خلال تحويلها أولاً عبر distribute()
و CuArray()
.
تذكر عند استيراد DistributedArrays.jl
استيراده عبر جميع العمليات باستخدام @everywhere
$ ./julia -p 4
julia> addprocs()
julia> @everywhere using DistributedArrays
julia> using CUDA
julia> B = ones(10_000) ./ 2;
julia> A = ones(10_000) .* π;
julia> C = 2 .* A ./ B;
julia> all(C .≈ 4*π)
true
julia> typeof(C)
Array{Float64,1}
julia> dB = distribute(B);
julia> dA = distribute(A);
julia> dC = 2 .* dA ./ dB;
julia> all(dC .≈ 4*π)
true
julia> typeof(dC)
DistributedArrays.DArray{Float64,1,Array{Float64,1}}
julia> cuB = CuArray(B);
julia> cuA = CuArray(A);
julia> cuC = 2 .* cuA ./ cuB;
julia> all(cuC .≈ 4*π);
true
julia> typeof(cuC)
CuArray{Float64,1}
في المثال التالي سنستخدم كل من DistributedArrays.jl
و CUDA.jl
لتوزيع مصفوفة عبر عمليات متعددة واستدعاء دالة عامة عليها.
function power_method(M, v)
for i in 1:100
v = M*v
v /= norm(v)
end
return v, norm(M*v) / norm(v) # or (M*v) ./ v
end
power_method
ينشئ باستمرار متجهًا جديدًا ويقوم بتطبيعه. لم نحدد أي نوع توقيع في إعلان الدالة، دعنا نرى ما إذا كان يعمل مع الأنواع المذكورة أعلاه:
julia> M = [2. 1; 1 1];
julia> v = rand(2)
2-element Array{Float64,1}:
0.40395
0.445877
julia> power_method(M,v)
([0.850651, 0.525731], 2.618033988749895)
julia> cuM = CuArray(M);
julia> cuv = CuArray(v);
julia> curesult = power_method(cuM, cuv);
julia> typeof(curesult)
CuArray{Float64,1}
julia> dM = distribute(M);
julia> dv = distribute(v);
julia> dC = power_method(dM, dv);
julia> typeof(dC)
Tuple{DistributedArrays.DArray{Float64,1,Array{Float64,1}},Float64}
لإنهاء هذا العرض القصير للحزم الخارجية، يمكننا النظر في MPI.jl
، وهو غلاف جوليّا لبروتوكول MPI. حيث أنه سيستغرق وقتًا طويلاً للنظر في كل دالة داخلية، سيكون من الأفضل ببساطة تقدير النهج المستخدم لتنفيذ البروتوكول.
اعتبر هذا السكربت التجريبي الذي يستدعي ببساطة كل عملية فرعية، ويقوم بإنشاء ترتيبها وعندما يتم الوصول إلى عملية الماستر، يتم تنفيذ مجموع الترتيبات.
import MPI
MPI.Init()
comm = MPI.COMM_WORLD
MPI.Barrier(comm)
root = 0
r = MPI.Comm_rank(comm)
sr = MPI.Reduce(r, MPI.SUM, root, comm)
if(MPI.Comm_rank(comm) == root)
@printf("sum of ranks: %s\n", sr)
end
MPI.Finalize()
mpirun -np 4 ./julia example.jl
- 1In this context, MPI refers to the MPI-1 standard. Beginning with MPI-2, the MPI standards committee introduced a new set of communication mechanisms, collectively referred to as Remote Memory Access (RMA). The motivation for adding rma to the MPI standard was to facilitate one-sided communication patterns. For additional information on the latest MPI standard, see https://mpi-forum.org/docs.