تستند هذه المقالة إلى نتائج تحقيقٍ في تتبع وحدة معالجة الرسومات على مستوى النواة، أُجري على مشكلة حقيقية في PyTorch (#154318) باستخدام أدوات eBPF uprobes. وقد نُشرت قواعد بيانات التتبع في مستودع Ingero مفتوح المصدر للتحقق المستقل.
TL؛ DR
قد يكون مُحمِّل البيانات في PyTorch أبطأ من 50 إلى 124 مرة من فهرسة الموترات المباشرة لأحمال عمل وحدة معالجة الرسومات في الذاكرة. قمنا بإعادة إنتاج مشكلة حقيقية في PyTorch على بطاقة RTX 4090، وتتبعنا كل استدعاء لواجهة برمجة تطبيقات CUDA وكل حدث في نواة Linux للعثور على السبب الجذري. لم تكن وحدة معالجة الرسومات بطيئة، بل كانت تعاني من نقص حاد في الموارد. أنتج مُحمِّل البيانات 200,000 تبديل سياق لوحدة المعالجة المركزية و300,000 عملية تخصيص صفحات في 40 ثانية، مما جعل وحدة معالجة الرسومات تنتظر بمعدل 301 مللي ثانية لكل عملية نقل بيانات، بينما من المفترض أن تستغرق العملية أجزاءً من الثانية.
المشكلة
مستخدم PyTorch وذكرت كان أداء DataLoader أبطأ من 7 إلى 22 مرة من فهرسة الموترات المباشرة في حالة استخدام استدلال MLP بسيط. حتى مع ضبط num_workers=12 و pin_memory=True و prefetch_factor=12، ظل الفارق كبيرًا. وبلغ استخدام وحدة معالجة الرسومات 10-20%.
لقد تمكّنا من إعادة إنتاج المشكلة. وكانت الفجوة أسوأ على أجهزتنا:
| الأسلوب | الوقت: | مقابل مباشر |
|---|---|---|
| فهرسة الموتر المباشر | 0.39 | 1x |
| DataLoader (shuffle=True) | 48.49 | أبطأ 124 مرات |
| مُحمِّل البيانات (مُحسَّن، 4 عمال، تثبيت الذاكرة) | 43.29 | أبطأ 111 مرات |
حجم العمل بسيط للغاية: 7 ملايين عينة، 100 ميزة، شبكة عصبية متعددة الطبقات ذات طبقتين، حجم الدفعة مليون عينة. يعالج النموذج دفعة واحدة في أجزاء من الثانية. إذن، أين يذهب الوقت؟
ما يُظهره برنامج nvidia-smi
لا توجد معلومات مفيدة. يتذبذب استخدام وحدة معالجة الرسومات بين 0% و30%. استخدام الذاكرة مستقر. درجة الحرارة طبيعية. من الواضح أن وحدة معالجة الرسومات لا تُستغل بالكامل، لكن أداة nvidia-smi لا تستطيع تحديد السبب.
ما يُظهره torch.profiler
حاول المراسل استخدام أداة تحليل الأداء المدمجة في PyTorch، لكنه لم يحصل على أي بيانات تتبع ذات مغزى. وهذا إحباط شائع - إذ يمكن لأدوات تحليل الأداء على مستوى التطبيق أن تُظهر لك نواة CUDA التي تعمل، لكنها لا تستطيع رؤية جدولة جانب المضيف والذاكرة وأحداث دورة حياة العملية التي تحدد ما إذا كانت البيانات تصل إلى وحدة معالجة الرسومات في الوقت المناسب.
ما يُظهره تتبع مستوى النواة
أجرينا الاختبار المعياري مع تتبع استدعاءات واجهة برمجة تطبيقات CUDA (عبر eBPF uprobes على libcudart.so) وأحداث نواة لينكس (تبديل سياق المجدول، وتخصيص صفحات الذاكرة، وتفرع العمليات) في آنٍ واحد. النتائج توضح الصورة كاملة.
شرح فيديو كامل: https://asciinema.org/a/RGwhPeXAPJdhXqxp
في الفيديو، قمنا بربط نموذج LLM مفتوح الوزن (MiniMax-M2.7) بقاعدة بيانات التتبع عبر بروتوكول سياق النموذج (MCP):
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonيُخبر ملف JSON الخاص بالتكوين عميل MCP بمكان العثور على خادم Ingero وقاعدة بيانات التتبع التي يجب تحميلها:
{ "mcpServers": { "ingero": { "command": "./bin/ingero", "args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"] } } }
يُتيح هذا لـ LLM الوصول المباشر إلى بيانات التتبع من خلال 7 أدوات: get_trace_stats، وget_causal_chains، وget_per_process_breakdown، وغيرها. يستطيع الذكاء الاصطناعي الاستعلام عن قاعدة البيانات، وربط أحداث CUDA ببيانات جدولة النواة، وإنتاج تشخيص بلغة بسيطة دون أي تحليل يدوي.
4 سلاسل سببية شديدة الخطورة
رصد محرك سلسلة الأسباب والنتائج 4 أنماط ذات خطورة عالية، جميعها لها نفس السبب الجذري:
[عالي] cudaStreamSync p99=42ms (1,638x p50=25us) - وحدة المعالجة المركزية 100% + 1,880 حدث sched_switch الجدول الزمني: [النظام] وحدة المعالجة المركزية 100% [المضيف] 1,880 تبديل سياق (21 ثانية بدون وحدة المعالجة المركزية) [CUDA] p99=42 مللي ثانية (1,638 × p50=25 ميكرو ثانية) السبب الجذري: عمال DataLoader يتنافسون على وحدة المعالجة المركزية، ضغط هائل على تخصيص الصفحات
[عالي] cudaLaunchKernel p99=24.67ms (349x p50=70us) - وحدة المعالجة المركزية 100%، الجذر: 34 حدث sched_switch [عالي] cuMemAlloc p99=627us (4.0x p50) - وحدة المعالجة المركزية 100% [عالي] cuLaunchKernel p99=106us (4.0x p50) - وحدة المعالجة المركزية 100%
إن قيمة p99 لـ cudaStreamSync أعلى بـ 1,638 مرة من قيمة p50. هذا ليس بطءًا في وحدة معالجة الرسومات - بل هو انتظار وحدة معالجة الرسومات للبيانات التي لا تصل أبدًا في الوقت المحدد.
الشكل 1: تحليل مُولّد بواسطة الذكاء الاصطناعي بعد تشغيل الأمر /investigate. استخدم النموذج أدوات Ingero السبعة للبرمجة متعددة المعاملات للاستعلام عن قاعدة بيانات التتبع، وأنتج شرحًا بلغة بسيطة مع توصيات قابلة للتنفيذ مستمدة مباشرة من بيانات التتبع.

تحليل كل عملية على حدة
هنا يتضح الأمر. تظهر العملية الرئيسية وعمالها الأربعة من نوع DataLoader ككيانات منفصلة:
العملية الرئيسية:
- cudaMemcpyAsync (نقل من المضيف إلى الجهاز): متوسط 301 مللي ثانية، أقصى 2.9 ثانية - cudaStreamSync: p99 = 42 مللي ثانية (عادةً 25 ميكروثانية) - 1,567 تبديل سياق، متوسط 16 مللي ثانية خارج وحدة المعالجة المركزية، أسوأ توقف 5 ثوانٍ - 799,018 تخصيص صفحة
عامل DataLoader 1: 52,863 تبديل سياق، 89,338 تخصيص صفحة، أسوأ توقف 5 ثوانٍ. عامل DataLoader 2: 50,638 تبديل سياق، 83,509 تخصيص صفحة، أسوأ توقف 5 ثوانٍ. عامل DataLoader 3: 49,361 تبديل سياق، 70,035 تخصيص صفحة، أسوأ توقف 5 ثوانٍ. عامل DataLoader 4: 38,862 تبديل سياق، 56,354 تخصيص صفحة، أسوأ توقف 5 ثوانٍ.
الإجمالي عبر العمال: ~191,000 تبديل سياق و~299,000 تخصيص صفحة في 40 ثانية.
ماذا يعني هذا
يقوم عمال DataLoader بثلاثة أشياء مكلفة يتجنبها الفهرسة المباشرة تمامًا:
- إعادة ترتيب وفهرسة البيانات: يُنشئ مُحمِّل البيانات مع خيار shuffle=True تبديلاً عشوائياً للفهارس، ثم يختار كل عامل الجزء الخاص به. يتطلب هذا الوصول العشوائي إلى الذاكرة عبر موتر العينات الكامل البالغ 7 ملايين عينة - وهو أمر سيء للغاية بالنسبة لموقع البيانات في ذاكرة التخزين المؤقت ويؤدي إلى أخطاء في الصفحات.
- التجميع والنسخ: يقوم كل عامل بتجميع العينات المتناثرة في موتر دفعي متجاور. وهذا يعني تخصيص ذاكرة جديدة (تخصيص الصفحات)، ونسخ البيانات من مواقع عشوائية (أخطاء ذاكرة التخزين المؤقت)، وإعادة ترتيب النتيجة إلى العملية الرئيسية عبر الذاكرة المشتركة أو قائمة الانتظار.
- التنافس على وحدة المعالجة المركزية: أربع عمليات عاملة بالإضافة إلى العملية الرئيسية على جهاز بأربع وحدات معالجة مركزية افتراضية تعني مقاطعة مستمرة. يتم إلغاء جدولة كل عملية عاملة 50,000 مرة. أسوأ حالة توقف هي 5 ثوانٍ - خلالها لا يكون لدى وحدة معالجة الرسومات أي شيء للمعالجة.
مع فهرسة مباشرة: يمثل X[i:i+batch_size] عرضًا بدون نسخ لموتر متجاور موجود بالفعل في الذاكرة. يؤدي .to(device) إلى تشغيل عملية نقل DMA واحدة من منطقة متجاورة واحدة. لا توجد عمليات معالجة فرعية، ولا خلط، ولا ترتيب، ولا نسخ بين العمليات، ولا تبديل سياق. تحصل وحدة معالجة الرسومات على البيانات في أجزاء من الثانية، وليس في مئات أجزاء من الثانية.
الإصلاح
بالنسبة لأحمال عمل وحدة معالجة الرسومات (GPU) الموجودة في الذاكرة حيث تتسع مجموعة البيانات بأكملها في ذاكرة الوصول العشوائي (RAM):
- لا تستخدم أداة تحميل البيانات. الفهرسة المباشرة باستخدام مصفوفة فهرسة مُرتبة مسبقًا أبسط وأسرع بمئة مرة:
indices = torch.randperm(num_samples) for i in range(0, num_samples, batch_size): batch = X[indices[i:i+batch_size]].to(device) output = model(batch)
- إذا كان لا بد من استخدام DataLoader، قم بمطابقة عدد العمليات (num_workers) مع عدد أنوية المعالج لديك مطروحًا منه 1. على جهاز رباعي النواة، يقلل num_workers=2 من التنازع. أضف persistent_workers=True لتجنب زيادة الحمل على عملية التفرع (fork overtime).
- بالنسبة لمجموعات البيانات الأكبر من حجم الذاكرة عندما يكون DataLoader ضروريًا، فإن عنق الزجاجة الحقيقي يتحول إلى عمليات الإدخال/الإخراج للقرص. استخدم prefetch_factor=2 (وليس أعلى - فزيادة الجلب المسبق تعني زيادة الضغط على الذاكرة) وتأكد من أن وحدة التخزين لديك قادرة على مواكبة ذلك.
الصورة الأكبر
يُظهر هذا التحقيق نمطًا نراه باستمرار في أحمال عمل وحدة معالجة الرسومات: وحدة معالجة الرسومات سريعة، والمعالج المضيف هو عنق الزجاجة، ولا تستطيع مقاييس وحدة معالجة الرسومات رصد ذلك. أبلغت أداة nvidia-smi عن انخفاض في الاستخدام لكنها لم تستطع تفسير السبب. رصدت أداة torch.profiler نواة CUDA لكنها أغفلت 200,000 عملية تبديل سياق تحدث في مساحة المستخدم.
كانت الطريقة الوحيدة لرؤية الصورة الكاملة هي تتبع كلا الجانبين في آنٍ واحد - استدعاءات واجهة برمجة تطبيقات CUDA على مستوى المكتبة وأحداث جدولة نواة لينكس - وربطها زمنيًا وبمعرف العملية. يروي التسلسل السببي "CPU 100% -> 1,880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms" القصة كاملةً في سطر واحد. لولا تتبع المكدس المتقاطع، لظل هذا لغزًا محيرًا - كما كان الحال بالنسبة للمُبلغ الأصلي الذي أمضى أسابيع في محاولة تصحيحه.
الشكل 2: عند سؤال النموذج "ما هي المشكلة الأساسية؟"، يُشير إلى أن زيادة تحميل وحدة المعالجة المركزية تُسبب تأخيرات في جدولة جانب المضيف. انخفض زمن تشغيل cudaLaunchKernel من 73 ميكروثانية إلى 25.8 مللي ثانية (أبطأ بمقدار 356 مرة) لأن وحدة المعالجة المركزية لم تتمكن من جدولة التشغيل في الوقت المناسب.

جربها بنفسك
أعد إنتاج المعيار:
import torch, time from torch.utils.data import DataLoader X = torch.randn(7_000_000, 100) model = torch.nn.Sequential( torch.nn.Linear(100, 512), torch.nn.ReLU(), torch.nn.Linear(512, 512), torch.nn.ReLU(), torch.nn.Linear(512, 10) ).cuda() # المسار السريع start = time.time() with torch.no_grad(): for i in range(0, len(X), 1_048_576): model(X[i:i+1_048_576].cuda()) torch.cuda.synchronize() print(f'Direct: {time.time()-start:.3f}s') # المسار البطيء loader = DataLoader(X, batch_size=1_048_576, shuffle=True) start = time.time() with torch.no_grad(): for batch in loader: model(batch.cuda()) torch.cuda.synchronize() print(f'DataLoader: {time.time()-start:.3f}s')
تتبع مع إنجيرو لمعرفة ما يحدث في الخفاء:
استنساخ جيت https://github.com/ingero-io/ingero.git انتقل إلى مجلد ingero وقم بتشغيل الأمر make build ثم نفّذ الأمر sudo ./bin/ingero trace --duration 60s # في نافذة طرفية واحدة، شغّل python3 benchmark.py # في نافذة طرفية أخرى، شغّل ./bin/ingero explain --since 60s # بعد اكتمال الاختبار
أو يمكنك تخطي عملية إعادة الإنتاج واستكشاف بيانات التتبع مباشرةً. قاعدة بيانات التحقيق (764 كيلوبايت) موجودة في المستودع التالي:
# عرض سلاسل السببية من التحقيق ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m # تفصيل لكل عملية (انظر عمال DataLoader مقابل العملية الرئيسية) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m # ربط مساعد الذكاء الاصطناعي الخاص بك لإجراء تحقيق تفاعلي ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
التحقيق باستخدام الذكاء الاصطناعي (موصى به). أسرع طريقة لتحليل البيانات هي ربط أي نظام ذكاء اصطناعي متوافق مع MCP مباشرةً بقاعدة البيانات. لا حاجة للتحليل اليدوي.
أنشئ ملف إعدادات:
cat > /tmp/ingero-mcp-dataloader.json << 'EOF' { "mcpServers": { "ingero": { "command": "./bin/ingero", "args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"] } } } EOF
ثم قم بتوصيل النموذج الخاص بك:
# مع Ollama + MiniMax (ما استخدمناه في الفيديو) ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json # مع Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json # مع أي عميل متوافق مع MCP # أضف الإعدادات أعلاه إلى إعدادات MCP الخاصة بذكائك الاصطناعي
اكتب /investigate لتشغيل التحليل الموجه، أو اطرح أي سؤال: "ما الذي تسبب في نقص موارد وحدة معالجة الرسومات؟" يتمتع الذكاء الاصطناعي بإمكانية الوصول إلى 7 أدوات تستعلم مباشرة عن قاعدة بيانات التتبع.
جيثب: github.com/ingero-io/ingero
العدد الأصلي: pytorch/pytorch#154318
شرح فيديو توضيحي: https://asciinema.org/a/RGwhPeXAPJdhXqxp
تم إجراء التحقيق على TensorDock RTX 4090 (24GB)، Ubuntu 22.04، PyTorch 2.10.0+cu128.













