บทความนี้อ้างอิงจากผลการวิจัยจากการสืบสวนการแทรกแซงที่ระดับเคอร์เนลโดยใช้ eBPF uprobes ในปัญหา PyTorch จริง (#154318) ฐานข้อมูลการแทรกแซงเผยแพร่ใน仓庫 Ingero ที่เปิดกว้างสำหรับการตรวจสอบอิสระ
TL;DR
DataLoader ของ PyTorch สามารถช้ากว่าการอ้างอิงเทนเซอร์โดยตรง 50-124 เท่าสำหรับงานในหน่วยความจำ GPU เราได้ทำซ้ำปัญหา PyTorch จริงบน RTX 4090 และติดตามการเรียก API CUDA ทุกครั้งและเหตุการณ์เคอร์เนล Linux เพื่อค้นหาสาเหตุของปัญหา GPU ไม่ช้า – มันถูกทอดทิ้ง ให้ทำงาน DataLoader ส่งผลให้เกิดการเปลี่ยนบริบท CPU 200,000 ครั้งและการจัดสรรหน้า 300,000 ครั้งใน 40 วินาที ทำให้ GPU ต้องรอเฉลี่ย 301 มิลลิวินาทีในการถ่ายโอนข้อมูลที่ควรใช้เวลาไมโครวินาที
ปัญหา
ผู้ใช้ PyTorch รายหนึ่ง รายงาน ว่า DataLoader ช้ากว่าการอ้างอิงเทนเซอร์โดยตรง 7-22 เท่าสำหรับงานอนุมาน MLP ที่ง่าย แม้ว่าจะมี num_workers=12, pin_memory=True และ prefetch_factor=12 ช่องว่างยังคงมาก การใช้ GPU อยู่ที่ 10-20%
เราทำซ้ำมัน ช่องว่างยังคงมากขึ้นบนฮาร์ดแวร์ของเรา:
| วิธีการ | เวลา | เทียบกับการอ้างอิงโดยตรง |
|---|---|---|
| การอ้างอิงเทนเซอร์โดยตรง | 0.39 วินาที | 1 เท่า |
| DataLoader (shuffle=True) | 48.49 วินาที | ช้ากว่า 124 เท่า |
| DataLoader (ปรับให้เหมาะสม, 4 คนงาน, pin_memory) | 43.29 วินาที | ช้ากว่า 111 เท่า |
งานนี้เป็นเรื่องง่าย: 7 ล้านตัวอย่าง, 100 คุณลักษณะ, 2-ชั้น MLP, ขนาดแบตช์ 1 ล้าน มอดูลประมวลผลแบตช์ในไม่กี่วินาที ดังนั้นเวลาไปไหน?
สิ่งที่ nvidia-smi แสดง
ไม่มีอะไรที่มีประโยชน์ การใช้ GPU มีเสถียรภาพระหว่าง 0% ถึง 30% การใช้หน่วยความจำมีเสถียรภาพ อุณหภูมิเป็นปกติ GPU อยู่ในสถานะที่ไม่ได้ใช้งาน แต่ nvidia-smi ไม่สามารถบอกได้ว่าทำไม
สิ่งที่ torch.profiler แสดง
ผู้รายงานพยายามใช้เครื่องมือวิเคราะห์ประสิทธิภาพที่มีอยู่ใน PyTorch และ “ไม่ได้รับข้อมูลการวิเคราะห์ที่มีความหมาย” สิ่งนี้เป็นความผิดหวังทั่วไป – เครื่องมือวิเคราะห์ระดับแอปพลิเคชันสามารถแสดงให้เห็นว่าคอร์ CUDA กำลังทำงาน แต่ไม่สามารถเห็นเหตุการณ์การจัดตารางการทำงานของโฮสต์ การจัดการหน่วยความจำ และการดำเนินกระบวนการที่กำหนดว่าข้อมูลจะมาถึง GPU เมื่อไหร่
สิ่งที่การแทรกแซงที่ระดับเคอร์เนลแสดง
เราทำการวัดประสิทธิภาพในขณะที่ติดตามการเรียก API CUDA (ผ่าน eBPF uprobes บน libcudart.so) และเหตุการณ์เคอร์เนล Linux (การเปลี่ยนบริบท, การจัดสรรหน้า, การสร้างกระบวนการ) พร้อมกัน ผลลัพธ์เล่าเรื่องราวที่สมบูรณ์
วิดีโอทัวร์เต็มรูปแบบ: https://asciinema.org/a/RGwhPeXAPJdhXqxp
ใน视频 เราเชื่อมต่อ LLM ที่เปิดกว้าง (MiniMax-M2.7) กับฐานข้อมูลการแทรกแซงผ่าน MCP (Model Context Protocol):
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonไฟล์ JSON คอนฟิกบอก MCP client ว่าควรหาสerver 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 และอื่นๆ AI สามารถสืบค้นฐานข้อมูล, สอดคล้องกับเหตุการณ์ 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 ต่อสู้กันเพื่อ CPU, ความกดดันการจัดสรรหน้ามาก
[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 นั่นไม่ใช่ความช้าของ GPU – นั่นคือ GPU ที่รอข้อมูลที่ไม่มาถึงทันเวลา
รูปที่ 1: การวิเคราะห์ที่สร้างโดย AI หลังการวิ่ง /investigate มอดูลใช้เครื่องมือ MCP 7 ชิ้นของ Ingero เพื่อสืบค้นฐานข้อมูลการแทรกแซงและสร้างคำอธิบายภาษาธรรมดาที่มีคำแนะนำที่สามารถดำเนินการได้โดยตรงจากข้อมูลการแทรกแซง

การแบ่งกระบวนการ
สิ่งนี้ชัดเจน กระบวนการหลักและคนงาน DataLoader 4 คนของมันปรากฏเป็นหน่วยที่แยกจากกัน:
กระบวนการหลัก:
- cudaMemcpyAsync (การถ่ายโอนจากโฮสต์ไปยังอุปกรณ์): ค่าเฉลี่ย 301ms, สูงสุด 2.9 วินาที - cudaStreamSync: p99 = 42ms (ปกติ 25us) - 1,567 context switches, ค่าเฉลี่ย 16ms off-CPU, การหยุดชั่วคราวที่แย่ที่สุด 5 วินาที - 799,018 page allocations
DataLoader worker 1: 52,863 context switches, 89,338 page allocations, การหยุดชั่วคราวที่แย่ที่สุด 5s DataLoader worker 2: 50,638 context switches, 83,509 page allocations, การหยุดชั่วคราวที่แย่ที่สุด 5s DataLoader worker 3: 49,361 context switches, 70,035 page allocations, การหยุดชั่วคราวที่แย่ที่สุด 5s DataLoader worker 4: 38,862 context switches, 56,354 page allocations, การหยุดชั่วคราวที่แย่ที่สุด 5s
ทั้งหมดทั่วคนงาน: ~191,000 context switches และ ~299,000 page allocations ใน 40 วินาที
สิ่งที่หมายถึง
คนงาน DataLoader ทำสิ่งที่มีค่าใช้จ่ายสูงสามสิ่งที่การอ้างอิงโดยตรงหลีกเลี่ยง:
- การเข้าถึงแบบสุ่มและดัชนี: DataLoader ที่มี shuffle=True สร้างการเรียงสับเปลี่ยนแบบสุ่มของดัชนี จากนั้นคนงานแต่ละคนเลือกชิ้นส่วนของมัน ซึ่งต้องเข้าถึงหน่วยความจำแบบสุ่มทั่วทั้งเทนเซอร์ 7 ล้านตัวอย่าง – ซึ่งไม่ดีสำหรับการจัดเก็บในแคชและทำให้เกิด page faults
- การรวบรวมและคัดลอก: คนงานแต่ละคนรวบรวมตัวอย่างที่กระจายอยู่เข้าด้วยกันเป็นเทนเซอร์แบตช์ที่ต่อเนื่องกัน ซึ่งหมายถึงการจัดสรรหน่วยความจำใหม่ (page allocations), คัดลอกข้อมูลจากตำแหน่งแบบสุ่ม (cache misses) และการอนุกรมข้อมูลกลับไปยังกระบวนการหลักผ่านหน่วยความจำร่วมหรือคิว
- การแข่งขันสำหรับ CPU: คนงาน 4 คน + กระบวนการหลักบนเครื่อง 4-vCPU หมายถึงการพรีเอมป์ตแบบต่อเนื่อง คนงานแต่ละคนถูกพรีเอมป์ต 50,000 ครั้ง การหยุดชั่วคราวที่แย่ที่สุดคือ 5 วินาที – ในช่วงเวลานั้น GPU ไม่มีอะไรที่จะประมวลผล
การอ้างอิงโดยตรง: X[i:i+batch_size] เป็นมุมมองที่ไม่มีการคัดลอกของเทนเซอร์ที่ต่อเนื่องกันอยู่แล้วในหน่วยความจำ .to(device)觸发การถ่ายโอน DMA จากภูมิภาคที่ต่อเนื่องกันเพียงครั้งเดียว ไม่มีคนงาน, ไม่มีการสุ่ม, ไม่มีการรวบรวม, ไม่มีการคัดลอกข้ามกระบวนการ, ไม่มีการเปลี่ยนบริบท ไม่มีการหยุดชั่วคราว GPU ได้รับข้อมูลในไมโครวินาที ไม่ใช่หลายร้อยมิลลิวินาที
การแก้ไข
สำหรับงานในหน่วยความจำ GPU ที่ทั้งเซตข้อมูลสามารถใส่ใน RAM:
- ไม่ใช้ 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)
- หากคุณต้องใช้ DataLoader, จับคู่ num_workers กับแกน CPU จริงของคุณลบ 1 บนเครื่อง 4-แกน จับคู่ num_workers=2 เพื่อลดการขัดแย้งกัน เพิ่ม persistent_workers=True เพื่อหลีกเลี่ยงการสร้างกระบวนการใหม่
- สำหรับเซตข้อมูลที่ใหญ่กว่าหน่วยความจำ ที่ต้องใช้ DataLoader จุดอุดตันจริงๆ คือ I/O ดิสก์ ใช้ prefetch_factor=2 (ไม่ใช่มากกว่านั้น – การพรีเฟตชิ่งที่มากขึ้นหมายถึงความกดดันหน่วยความจำที่มากขึ้น) และตรวจสอบให้แน่ใจว่าระบบจัดเก็บข้อมูลของคุณสามารถรองรับได้
ภาพรวมที่ใหญ่กว่า
การสอบสวนนี้แสดงรูปแบบที่เราเห็นบ่อยๆ ในงาน GPU: GPU เร็ว, โฮสต์ช้า, และเมตริก GPU ไม่สามารถมองเห็นได้ nvidia-smi รายงานการใช้งานต่ำ แต่ไม่สามารถอธิบายได้ว่าทำไม
วิธีเดียวที่จะเห็นภาพที่สมบูรณ์คือติดตามทั้งสองด้านพร้อมกัน – การเรียก API CUDA และเหตุการณ์เคอร์เนล Linux – และสอดคล้องกันโดยเวลาและ ID กระบวนการ โซ่สาเหตุ “CPU 100% -> 1,880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” เล่าเรื่องราวที่สมบูรณ์ในหนึ่งบรรทัด หากไม่มีการแทรกแซงที่ครอบคลุม สิ่งนี้จะยังคงเป็นปริศนา – เช่นเดียวกับที่ผู้รายงานเดิมที่ใช้เวลาหลายสัปดาห์ในการแก้ปัญหา
รูปที่ 2: เมื่อถาม “สาเหตุหลักคืออะไร?” มอดูลระบุว่า CPU มีการจัดสรรมากเกินไปทำให้เกิดความล่าช้าในการจัดตารางการทำงานของโฮสต์ cudaLaunchKernel ล่าช้าจาก 73us ไปเป็น 25.8ms (356 เท่า)

ลองด้วยตัวเอง
ทำซ้ำการวัดประสิทธิภาพ:
import torch, time from torch.utils.data import DataLoaderX = 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()
# Fast path 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')
# Slow path 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 # in one terminal python3 benchmark.py # in another terminal ./bin/ingero explain --since 60s # after benchmark completes
หรือเลี่ยงการทำซ้ำและสำรวจฐานข้อมูลการแทรกแซงของเราโดยตรง ฐานข้อมูลการสอบสวน (764KB) อยู่ใน仓庫:
# View causal chains from the investigation ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m# Per-process breakdown (see DataLoader workers vs main process) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m
# Connect your AI assistant for interactive investigation ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
สืบค้นด้วย AI (แนะนำ). วิธีที่เร็วที่สุดในการวิเคราะห์การแทรกแซงคือการเชื่อมต่อ AI ที่รองรับ MCP ใดๆ กับฐานข้อมูลโดยตรง ไม่ต้องมีการวิเคราะห์แบบมือ
สร้างไฟล์คอนฟิก:
cat > /tmp/ingero-mcp-dataloader.json << 'EOF'
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
EOF
จากนั้นเชื่อมต่อมอดูลของคุณ:
# With Ollama + MiniMax (what we used in the video) ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json# With Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json
# With any MCP-compatible client # Add the config above to your AI's MCP settings
พิมพ์ /investigate เพื่อกระตุ้นการวิเคราะห์ที่มีคำแนะนำ หรือถามคำถามใดๆ: “สาเหตุของการขาดแคลน GPU คืออะไร?” AI มีเครื่องมือ 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.













