تقارير الرؤى

124x أبطأ: ماذا يفعل PyTorch DataLoader في المستوى التشغيلي

mm
A conceptual widescreen illustration of an hourglass containing glowing digital data streams and circuit patterns, with the bottom half featuring a large data block and a GPU hardware component sitting idle to the side, representing data processing delays and GPU starvation.

هذا المقال مبني على نتائج تحقيق تم إجراؤه على مستوى تشغيلي لخروق حقيقي في PyTorch (#154318) باستخدام eBPF uprobes. يتم نشر قواعد بيانات التتبع في مستودع Ingero مفتوح المصدر للتأكد المستقل.

ملخص

يمكن أن يكون PyTorch’s DataLoader أبطأ 50-124 مرة من التهيئة المباشرة للتنسور على وحدات معالجة الرسومات في الذاكرة. لقد أعدنا إنتاج مشكلة حقيقية في PyTorch على RTX 4090 وتبعنا كل مكالمة CUDA API وحدث نواة لينكس للعثور على السبب الجذري. لم تكن وحدات معالجة الرسومات بطيئة – كانت جائعة. أ.generated 200,000 تبديل سياق CPU و 300,000 تخصيص صفحة في 40 ثانية، تاركة وحدات معالجة الرسومات في انتظار متوسط 301ms لكل نقل بيانات يجب أن يستغرق microseconds.

المشكلة

أبلغ مستخدم PyTorch أن DataLoader كان أبطأ 7-22 مرة من التهيئة المباشرة للتنسور لمحمل الاستدلال البسيط. حتى مع num_workers=12 و pin_memory=True و prefetch_factor=12، ظلت الفجوة ضخمة. استهلاك وحدات معالجة الرسومات يتراوح بين 10-20٪.

لقد أعدنا إنتاجها. كانت الفجوة أسوأ على أجهزتنا:

الطريقة الوقت مقارنة بالتهيئة المباشرة
التهيئة المباشرة للتنسور 0.39s 1x
DataLoader (shuffle=True) 48.49s 124x أبطأ
DataLoader (مُحسّن، 4 عمال، pin_memory) 43.29s 111x أبطأ

الوحدات هي بسيطة: 7M عينة، 100 ميزة، 2-طبقة MLP، حجم الدفعة 1M. نموذج يعالج الدفعة في مللي ثانية.,所以 أين يذهب الوقت؟

ماذا يظهر nvidia-smi

لا شيء مفيد. استهلاك وحدات معالجة الرسومات يتأرجح بين 0٪ و 30٪. استخدام الذاكرة مستقر. درجة الحرارة جيدة. وحدات معالجة الرسومات واضحة تحت الاستخدام، ولكن nvidia-smi لا يمكن أن يخبرك لماذا.

ماذا يظهر torch.profiler

حاول المُبلّغ محول PyTorch المدمج و “حصل على keine بيانات تتبع مفيدة”. هذا هو إحباط شائع – يمكن لمحولات التطبيق أن تُظهر لك ماذا يعمل نووي CUDA، ولكنها لا يمكن أن ترى جدولة المستضيف، أحداث الذاكرة، وأحداث دورة الحياة التي تحدد ما إذا كانت البيانات تصل إلى وحدات معالجة الرسومات في الوقت المناسب.

ماذا يظهر التتبع على مستوى النواة

قمنا بتشغيل اختبار الأداء أثناء تتبع مكالمات CUDA API (من خلال eBPF uprobes على libcudart.so) وأحداث نواة لينكس (تبديل سياق المُجدول، تخصيص صفحة الذاكرة، إنشاء عملية) في نفس الوقت. النتائج تحكي القصة الكاملة.

فيديو كامل: https://asciinema.org/a/RGwhPeXAPJdhXqxp

في الفيديو، قمنا بتوصيل 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 أنماط عالية الشدة، جميعها ذات نفس السبب الجذري:

[HIGH] cudaStreamSync p99=42ms (1,638x p50=25us) - CPU 100% + 1,880 sched_switch events
Timeline:
[SYSTEM] CPU 100%
[HOST ] 1,880 context switches (21s off-CPU)
[CUDA ] p99=42ms (1,638x p50=25us)
Root cause: DataLoader workers fighting for CPU, massive page allocation pressure
[HIGH] cudaLaunchKernel p99=24.67ms (349x p50=70us) - CPU 100%
Root: 34 sched_switch events

[HIGH] cuMemAlloc p99=627us (4.0x p50) - CPU 100%
[HIGH] cuLaunchKernel p99=106us (4.0x p50) - CPU 100%

cudaStreamSync p99 هو 1,638 مرة p50. هذا ليس بطء وحدات معالجة الرسومات – هذا هو انتظار وحدات معالجة الرسومات لبيانات لا تصل أبداً في الوقت المناسب.

الشكل 1: تحليل الذكاء الاصطناعي بعد تشغيل /investigate. استخدم النموذج أدوات MCP السبع لاستفسار قاعدة بيانات التتبع و أنتج شرح بلغة عادية مع توصيات قابلة للتنفيذ مستمدة مباشرة من بيانات التتبع.

التحليل لكل عملية

هذا هو المكان الذي يصبح واضحاً. العملية الرئيسية و 4 عمال DataLoader مرئية ككيانات منفصلة:
العملية الرئيسية:

- cudaMemcpyAsync (نقل من المضيف إلى الجهاز): متوسط 301ms، أقصى 2.9 ثانية
- cudaStreamSync: p99 = 42ms (عادة 25us)
- 1,567 context switches، متوسط 16ms off-CPU، أسوأ إيقاف 5 ثوان
- 799,018 تخصيص صفحة
DataLoader worker 1: 52,863 context switches، 89,338 تخصيص صفحة، أسوأ إيقاف 5s
DataLoader worker 2: 50,638 context switches، 83,509 تخصيص صفحة، أسوأ إيقاف 5s
DataLoader worker 3: 49,361 context switches، 70,035 تخصيص صفحة، أسوأ إيقاف 5s
DataLoader worker 4: 38,862 context switches، 56,354 تخصيص صفحة، أسوأ إيقاف 5s

المجموع عبر العمالة: ~191,000 context switches و ~299,000 تخصيص صفحة في 40 ثانية.

ماذا يعني هذا

عمال DataLoader يقومون بثلاثة أشياء مكلفة التي تتجنبها التهيئة المباشرة تماما:

  1. الخلط والفهرسة: DataLoader مع shuffle=True يولد تمزق عشوائي للفهرسة، ثم يختار كل عامل جزءه. هذا يتطلب الوصول العشوائي إلى الذاكرة عبر كامل التنسور 7M-عينة – سيء للذاكرة المخبأة ويؤدي إلى أخطاء الصفحة.
  2. التركيب والنسخ: كل عامل يجمع عينات متفرقة في تنسور دفعة متواصل. هذا يعني تخصيص ذاكرة جديدة (تخصيص صفحة)، نسخ البيانات من مواقع عشوائية (خطأ الذاكرة المخبأة)، وتسلسل النتيجة مرة أخرى إلى العملية الرئيسية عبر الذاكرة المشتركة أو الطابور.
  3. التنافس على CPU: 4 عمال + العملية الرئيسية على جهاز 4-vCPU يعني الاستبدال المستمر. كل عامل يتم إيقافه 50,000 مرة. أسوأ إيقاف هو 5 ثوان – خلالها لا تملك وحدات معالجة الرسومات أي شيء لمعالجته.

مع التهيئة المباشرة: X[i:i+batch_size] هو نظرة بدون نسخ لتنسور متواصل بالفعل في الذاكرة. .to(device) يؤدي إلى نقل DMA واحد من منطقة متواصلة واحدة. لا عمال، لا خلط، لا تركيب، لا نسخ متقاطع، لا تبديل سياق. وحدات معالجة الرسومات تحصل على البيانات في microseconds، وليس مئات من المللي ثانية.

الإصلاح

لأحمال وحدات معالجة الرسومات في الذاكرة حيث يتناسب كامل مجموعة البيانات في RAM:

  1. لا تستخدم DataLoader. التهيئة المباشرة مع مصفوفة فهرسة مسبقة هي أبسط وأسرع 100 مرة:
    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)
    
  2. إذا كنت يجب استخدام DataLoader، تطابق num_workers مع نواة CPU الفعلية ناقص 1. على جهاز 4-نواة، num_workers=2 يقلل من الصراع. أضف persistent_workers=True لتجنب عبء الفورك.
  3. لمجموعات البيانات الأكبر من الذاكرة حيث DataLoader ضروري، يتحول الحاجز الحقيقي إلى إدخال/إخراج القرص. استخدم prefetch_factor=2 (ليس أعلى – المزيد من التمهيد يعني ضغط ذاكرة أكبر) و تأكد من أن تخزينك يمكنه مواكبة ذلك.

الصورة الأكبر

تحقيقنا هذا يُظهر نمطاً نراه دائماً في أحمال وحدات معالجة الرسومات: وحدات معالجة الرسومات سريعة، المضيف هو الحاجز، و معايير وحدات معالجة الرسومات لا يمكنها رؤية ذلك. أبلغ nvidia-smi عن استهلاك منخفض، لكنه لا يمكن أن يُخبرك لماذا. محول PyTorch أسرع نووي CUDA، لكنه فاته 200,000 تبديل سياق في المستخدم.

الطريقة الوحيدة لرؤية الصورة الكاملة هي تتبع كلا الجانبين في نفس الوقت – مكالمات CUDA API على مستوى المكتبة وأحداث جدولة نواة لينكس – وترجمة بينهما بالزمن و معرف العملية. سلسلة السببية “CPU 100% -> 1,880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” تحكي القصة الكاملة في سطر واحد. بدون تتبع متقاطع، هذا لكان يبقى لغزا – كما كان للباحث الأصلي الذي قضى أسابيع في تصحيح الأخطاء.

الشكل 2: عندما سُئل “ما هو القضية الأساسية؟”، يحدد النموذج الاشتراك المفرط للCPU مما يسبب تأخيرات جدولة المضيف. انتقلت cudaLaunchKernel من 73us إلى 25.8ms (356 مرة أبطأ) لأن CPU لم يكن قادراً على جدولة الإطلاق في الوقت المناسب.

جربها بنفسك

أعد إنتاج اختبار الأداء:

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')

تتبع مع Ingero لرؤية ما يحدث تحت الغطاء:

git clone https://github.com/ingero-io/ingero.git
cd ingero && make build
sudo ./bin/ingero trace --duration 60s # في نافذة طرفية
python3 benchmark.py # في نافذة طرفية أخرى
./bin/ingero explain --since 60s # بعد اكتمال اختبار الأداء

أو تجاوز الإعادة و استكشف بيانات التتبع مباشرة. قاعدة بيانات التحقيق (764KB) في المستودع:

# عرض سلاسل سببية من التحقيق
./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: 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.

ديفيد ميل هو مؤلف مشارك ومحافظ على Ingero، وهو وكلاء مفتوح المصدر eBPF لمراقبة CUDA-مستوى GPU. وهو متخصص في تتبع مستوى النواة من حمولة الإنتاج AI.