รายงาน

124x ช้ากว่า: สิ่งที่ PyTorch DataLoader ทำที่ระดับเคอร์เนล

mm
เพิ่ม Unite.AI ลงในแหล่งข้อมูลที่คุณต้องการบน Google
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.

บทความนี้อ้างอิงจากผลการวิจัยจากการสืบสวนการแทรกแซงที่ระดับเคอร์เนลโดยใช้ 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 ทำสิ่งที่มีค่าใช้จ่ายสูงสามสิ่งที่การอ้างอิงโดยตรงหลีกเลี่ยง:

  1. การเข้าถึงแบบสุ่มและดัชนี: DataLoader ที่มี shuffle=True สร้างการเรียงสับเปลี่ยนแบบสุ่มของดัชนี จากนั้นคนงานแต่ละคนเลือกชิ้นส่วนของมัน ซึ่งต้องเข้าถึงหน่วยความจำแบบสุ่มทั่วทั้งเทนเซอร์ 7 ล้านตัวอย่าง – ซึ่งไม่ดีสำหรับการจัดเก็บในแคชและทำให้เกิด page faults
  2. การรวบรวมและคัดลอก: คนงานแต่ละคนรวบรวมตัวอย่างที่กระจายอยู่เข้าด้วยกันเป็นเทนเซอร์แบตช์ที่ต่อเนื่องกัน ซึ่งหมายถึงการจัดสรรหน่วยความจำใหม่ (page allocations), คัดลอกข้อมูลจากตำแหน่งแบบสุ่ม (cache misses) และการอนุกรมข้อมูลกลับไปยังกระบวนการหลักผ่านหน่วยความจำร่วมหรือคิว
  3. การแข่งขันสำหรับ CPU: คนงาน 4 คน + กระบวนการหลักบนเครื่อง 4-vCPU หมายถึงการพรีเอมป์ตแบบต่อเนื่อง คนงานแต่ละคนถูกพรีเอมป์ต 50,000 ครั้ง การหยุดชั่วคราวที่แย่ที่สุดคือ 5 วินาที – ในช่วงเวลานั้น GPU ไม่มีอะไรที่จะประมวลผล

การอ้างอิงโดยตรง: X[i:i+batch_size] เป็นมุมมองที่ไม่มีการคัดลอกของเทนเซอร์ที่ต่อเนื่องกันอยู่แล้วในหน่วยความจำ .to(device)觸发การถ่ายโอน DMA จากภูมิภาคที่ต่อเนื่องกันเพียงครั้งเดียว ไม่มีคนงาน, ไม่มีการสุ่ม, ไม่มีการรวบรวม, ไม่มีการคัดลอกข้ามกระบวนการ, ไม่มีการเปลี่ยนบริบท ไม่มีการหยุดชั่วคราว GPU ได้รับข้อมูลในไมโครวินาที ไม่ใช่หลายร้อยมิลลิวินาที

การแก้ไข

สำหรับงานในหน่วยความจำ GPU ที่ทั้งเซตข้อมูลสามารถใส่ใน 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 จุดอุดตันจริงๆ คือ 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 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()

# 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.

เดวิด เมล เป็นผู้ร่วมเขียนและผู้ดูแล Ingero ซึ่งเป็นตัวแทน eBPF แบบโอเพ่นซอร์สสำหรับการสังเกตการณ์ GPU ระดับ CUDA เขามีความเชี่ยวชาญในการติดตามระดับเคอร์เนลของ AI ในการผลิต