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 والمنفذ الذي يجب أن تستخدمه العمال الآخرون للاتصال بهذا العامل.
Note

بينما تسعى جوليا عمومًا للحفاظ على التوافق مع الإصدارات السابقة، يعتمد توزيع الشيفرة إلى عمليات العمل على Serialization.serialize. كما هو مذكور في الوثائق المقابلة، لا يمكن ضمان أن يعمل هذا عبر إصدارات جوليا المختلفة، لذا يُنصح بأن تستخدم جميع العمال على جميع الآلات نفس الإصدار.

تتوفر الدوال addprocs، rmprocs، workers، وغيرها كوسيلة برمجية لإضافة وإزالة واستعلام العمليات في مجموعة.

julia> using Distributed

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

يجب تحميل الوحدة Distributed بشكل صريح على عملية الماستر قبل استدعاء addprocs. وهي متاحة تلقائيًا على عمليات العامل.

Note

لاحظ أن العمال لا يقومون بتشغيل برنامج بدء التشغيل ~/.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_40726566s، على الرغم من أنها في وضع عملية واحدة. في الحالات التي تصر فيها خوارزمية على إدخال 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;

إذا أنشأنا SharedArrays وقمنا بتوقيت هذه الدوال، نحصل على النتائج التالية (مع 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 يُستخدم لإطلاق عمال إضافيين على نفس المضيف، مما يستفيد من الأجهزة متعددة النوى والمعالجات.

لذا، سيحتاج مدير الكتلة الحد الأدنى إلى:

  • كن نوعًا فرعيًا من 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(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، والذي يتضمن:

  1. CUDA.jl يلتف حول مكتبات CUDA المختلفة ويدعم تجميع نوى جوليا لوحدات معالجة الرسوميات Nvidia.
  2. oneAPI.jl يلتف حول نموذج البرمجة الموحد oneAPI، ويدعم تنفيذ نوى جوليا على المسرعات المدعومة. حاليًا، يتم دعم نظام لينكس فقط.
  3. AMDGPU.jl يلتف حول مكتبات AMD ROCm ويدعم تجميع نوى جوليا لبطاقات AMD الرسومية. حاليًا، يتم دعم نظام لينكس فقط.
  4. مكتبات عالية المستوى مثل 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.