انتقل إلى المحتوى

شروحات

انقل محاكاة روبوتك من MuJoCo إلى كرت الرسوميات: 2,048 عالمًا متوازيًا مع MJWarp في 5 خطوات

شرح عملي مبني على مقال NVIDIA: تنقل مشهد MuJoCo لذراع SO-101 إلى MJWarp، وتتحقّق من مطابقة النتائج، وتضبط سعة التصادمات، ثم تقيس الإنتاجية بشكل صحيح.

انقل محاكاة روبوتك من MuJoCo إلى كرت الرسوميات: 2,048 عالمًا متوازيًا مع MJWarp في 5 خطوات

المستوى: متقدّم — يفترض أنك تعرف Python ومحاكاة الروبوتات.
وقت القراءة: نحو 10 دقائق.
هتحتاج: كرت رسوميات من NVIDIA يدعم CUDA، وتثبيت mujoco-warp، ومشهد MuJoCo بصيغة MJCF.

محاكاة الروبوتات في MuJoCo على المعالج ممتازة لتطوير روبوت واحد واختباره. لكن في التعلّم المعزّز يتحوّل السؤال من «كم تستغرق خطوة عالم واحد؟» إلى «كم عالمًا أستطيع تشغيله في وقت واحد؟». هنا يأتي MuJoCo Warp (اختصارًا MJWarp)، المبني على NVIDIA Warp، وهو ينقل نماذج MuJoCo المتوافقة إلى الـGPU لتقدّم دفعات كبيرة من العوالم معًا. الخطوات التالية تلخّص مقال NVIDIA، وتنقل ذراع SO-101 إلى 2,048 بيئة متوازية.

مصطلحان تحتاجهما: «العالم» (World) هو نسخة مستقلّة من المشهد وحالته. و«الإنتاجية الكلّية» (Aggregate Throughput) هي إجمالي خطوات العوالم المنجزة في الثانية، بينما «التأخّر» (Latency) هو زمن خطوة واحدة. قيمة MJWarp ليست بالضرورة أسرع خطوة لعالم واحد، بل تقدّم مئات أو آلاف العوالم معًا.

الخطوة 1: ابنِ خطّ أساس على المعالج

ابدأ بمشهد MuJoCo عادي: الذراع وطاولة ومكعّبان لمهمّة التكديس. المقال يستخدم مكعّبات بحافة 44 مم. حمّل النموذج وشغّل الحلقة المعتادة: احسب أوامر التحكّم مرّة لكل إطار، ثم نفّذ خطوات الفيزياء عدّة مرّات:

mjm = mujoco.MjModel.from_xml_path("scene_pick_place.xml")
mjd = mujoco.MjData(mjm)
fps = 50            # control rate
sim_substeps = 10   # physics steps per control frame
frame_dt = 1.0 / fps
mjm.opt.timestep = frame_dt / sim_substeps   # 0.002 s

ضبط الخطوة الزمنية قبل التشغيل مهمّ: بدونه ترث كل القياسات اللاحقة اختلافًا في الزمن المحاكى. وعرّف نجاح المهمّة بشرطين قابلين للقياس بعد استقرار المكعّبات: خطأ أفقي بين المركزين لا يزيد على 0.015 م، وفارق رأسي بين 0.035 و0.055 م.

الخطوة 2: تحقّق من تطابق عالم واحد على الـGPU

شغّل عالمًا واحدًا على الـGPU أوّلًا، مع بقاء الحلقة على المضيف، وقارن الرقمين نفسيهما:

import warp as wp
import mujoco_warp as mjw

wp.init()
device = wp.get_device()
m = mjw.put_model(mjm)
d = mjw.make_data(mjm, nworld=1, nconmax=128, njmax=300)
wp.copy(d.qpos, wp.array(mjd.qpos[None, :], dtype=wp.float32, device=device))
wp.copy(d.qvel, wp.array(mjd.qvel[None, :], dtype=wp.float32, device=device))
wp.copy(d.ctrl, wp.array(mjd.ctrl[None, :], dtype=wp.float32, device=device))
mjw.forward(m, d)

كل مصفوفة على الجهاز لها بُعد «العالم» في أوّلها، لذلك تُفهرس الحالة كـmjd.qpos[None, :]. وعند التوسّع لآلاف العوالم يتغيّر هذا البعد فقط، لا الاستدعاءات. والدالة mjw.put_model() تعمل أيضًا كفحص توافق: ترفع خطأ إذا استخدم النموذج ميزة غير مدعومة بدل أن تتجاهلها بصمت. في حلقة التحقّق تنسخ qpos وqvel إلى المضيف في كل خطوة فرعية، ثم تستدعي mujoco.mj_forward لتحديث الكميات المشتقّة. هذا مسار تحقّق وليس قياس إنتاجية.

الخطوة 3: اضبط سعة التصادمات والقيود

يحجز MJWarp مخازن التصادمات والقيود قبل التشغيل. تجاوز السعة يُبطل التجربة للتحقّق أو القياس حتى لو استمرّ التنفيذ بتحذير فقط. يستخدم المقال قيمًا ابتدائية لـSO-101 هي nconmax=128 وnjmax=300. قِس السعة عند أكثر لحظة تصادمًا في المهمّة، أي عندما يلمس الفكّان والطاولة المكعّب، وليس والذراع معلّقة في الهواء. والتجاوز يُبلَّغ عنه ولا يُرفع كاستثناء، إلا في mjw.put_data. أداة mjwarp-testspeed --measure_alloc تعرض ما استُهلك فعلًا وتوقف التشغيل عند أول تجاوز. اعتبر أي تجاوز فشلًا، وارفع الحدّ وأعد التشغيل قبل أن تثق بالنتائج.

الخطوة 4: كبّر إلى 2,048 عالمًا والتقط رسمًا بيانيًا لـCUDA

بعد نجاح تطابق العالم الواحد، أعد حجز البيانات بالحجم المستهدف وكرّر الحالة الابتدائية عبر الدفعة:

nworld = 2_048
d = mjw.make_data(mjm, nworld=nworld, nconmax=128, njmax=300)
wp.copy(d.qpos, wp.array(np.tile(mjd.qpos, (nworld, 1)), dtype=wp.float32, device=device))
wp.copy(d.qvel, wp.array(np.tile(mjd.qvel, (nworld, 1)), dtype=wp.float32, device=device))
wp.copy(d.ctrl, wp.array(np.tile(mjd.ctrl, (nworld, 1)), dtype=wp.float32, device=device))
mjw.forward(m, d)
with wp.ScopedCapture() as capture:
    mjw.step(m, d)
step_graph = capture.graph

تكرار الحالة نفسها لكل العوالم مناسب لقياس الإنتاجية. أما لو أردت عشوائية، فاكتب صفوفًا مختلفة في d.qpos. وتُعيد الرسوم البيانية (CUDA Graphs) استخدام المخازن الملتقطة، فحدّث d.ctrl في مكانه بين التشغيلات، والتقط رسمًا جديدًا بعد استبدال المخازن أو تغيير nworld أو إعادة بناء النموذج. الالتقاط يتطلّب CUDA.

الخطوة 5: تحقّق ثم قِس بشكل صحيح

إطلاقات الـGPU غير متزامنة، فمؤقّت ساذج يقيس سرعة إدخال Python للعمل في الطابور لا سرعة إنجاز الـGPU له. سخّن أوّلًا ثم زامن قبل المنطقة الزمنية وبعدها:

import time
for _ in range(10):            # warm-up: compilation, allocation, caches
    wp.capture_launch(step_graph)
wp.synchronize()
t0 = time.perf_counter()
for _ in range(200):
    wp.capture_launch(step_graph)
wp.synchronize()               # without this you time the queue, not the work
elapsed = time.perf_counter() - t0
print(f"{200 * nworld / elapsed:,.0f} world-steps/second")

أبلِغ عن الإنتاجية الكلّية وعن الميلي ثانية لكل خطوة دفعية مع حجم الدفعة معًا. النتائج تعتمد على المشهد والإعدادات والعتاد، ومقارنة زمن عالم واحد لا تثبت الإنتاجية الدفعية.

متى تستخدم ماذا؟

  • تحكّم أو تشغيل عن بُعد لروبوت واحد: MuJoCo على المعالج.
  • أقصى إنتاجية لفيزياء MuJoCo الخام: MJWarp (أو mjlab).
  • وصفات تدريب على JAX: MuJoCo Playground أو MJX بالخيار impl='warp'.
  • حلّالات متعدّدة وتكامل مع Isaac Lab: Newton، وهو موضوع المقال التالي في سلسلة NVIDIA.

هذا المقال لا يدرّب سياسة (Policy)، بل يجهّز بيئة المحاكاة ويوسّعها. ولتدريب سياسات فوق فيزياء MJWarp، يذكر المقال Isaac Lab عبر Newton، وmjlab، وMuJoCo Playground.

جرّب بنفسك

ثبّت الحزمة بالأمر pip install mujoco-warp، وخذ مشهد MJCF لديك، ومرّر نموذجك إلى mjw.put_model لتعرف إن كان متوافقًا. ثم اقِس الإنتاجية بحجوم دفعة متدرّجة (1 و64 و1024 و2048) على عتادك، وسجّل الميلي ثانية للخطوة والإنتاجية معًا. وراجع المقال الأصلي للاطّلاع على الشيفرة المرافقة. وفيه ملاحظة تفيد بأن رابط مستودع الشيفرة المرافق يحتاج تأكيدًا، فتحقّق منه بنفسك قبل الاعتماد عليه.

المصادر: Hugging Face — NVIDIA — 2026-09-23