Bài viết này dựa trên các phát hiện từ một cuộc điều tra dấu vết cấp kernel GPU được thực hiện trên một vấn đề thực sự của PyTorch (#154318) sử dụng eBPF uprobes. Các cơ sở dữ liệu dấu vết được xuất bản trong kho mã nguồn mở Ingero để xác minh độc lập.
Tóm Tắt
DataLoader của PyTorch có thể chậm hơn 50-124 lần so với chỉ mục tensor trực tiếp cho các công việc GPU trong bộ nhớ. Chúng tôi đã tái tạo một vấn đề thực sự của PyTorch trên một RTX 4090 và theo dõi mọi cuộc gọi API CUDA và sự kiện kernel Linux để tìm ra nguyên nhân gốc rễ. GPU không chậm – nó đang đói. Các công nhân DataLoader đã tạo ra 200.000 chuyển đổi ngữ cảnh CPU và 300.000 phân bổ trang trong 40 giây, khiến GPU chờ đợi trung bình 301ms cho mỗi chuyển giao dữ liệu nên mất micro giây.
Vấn Đề
Một người dùng PyTorch đã báo cáo rằng DataLoader chậm hơn 7-22 lần so với chỉ mục tensor trực tiếp cho một công việc suy luận MLP đơn giản. Ngay cả với num_workers=12, pin_memory=True và prefetch_factor=12, khoảng cách vẫn còn rất lớn. Sử dụng GPU nằm ở mức 10-20%.
Chúng tôi đã tái tạo nó. Khoảng cách thậm chí còn tồi tệ hơn trên phần cứng của chúng tôi:
| Phương Pháp | Thời Gian | So Với Trực Tiếp |
|---|---|---|
| Chỉ mục tensor trực tiếp | 0,39 giây | 1 lần |
| DataLoader (trộn=True) | 48,49 giây | Chậm hơn 124 lần |
| DataLoader (tối ưu hóa, 4 công nhân, pin_memory) | 43,29 giây | Chậm hơn 111 lần |
Công việc là nhỏ: 7M mẫu, 100 tính năng, 2 lớp MLP, kích thước batch 1M. Mô hình xử lý một batch trong vài mili giây. Vậy thời gian đi đâu?
Điều nvidia-smi Hiện
Không có gì hữu ích. Sử dụng GPU dao động giữa 0% và 30%. Sử dụng bộ nhớ ổn định. Nhiệt độ ổn định. GPU rõ ràng bị underutilized, nhưng nvidia-smi không thể cho bạn biết tại sao.
Điều torch.profiler Hiện
Người báo cáo đã thử bộ profiler tích hợp của PyTorch và “không nhận được bất kỳ dữ liệu dấu vết có ý nghĩa nào.” Đây là một sự thất vọng phổ biến – các bộ profiler cấp ứng dụng có thể cho bạn thấy các kernel CUDA đang chạy, nhưng chúng không thể nhìn thấy việc lập lịch trình phía host, sự kiện bộ nhớ và quá trình xảy ra quyết định liệu dữ liệu có đến được GPU đúng hạn hay không.
Điều Dấu Vết Cấp Kernel Hiện
Chúng tôi đã chạy điểm chuẩn trong khi theo dõi cả cuộc gọi API CUDA (thông qua eBPF uprobes trên libcudart.so) và sự kiện kernel Linux (chuyển đổi ngữ cảnh lập lịch, phân bổ trang) đồng thời. Kết quả cho biết câu chuyện hoàn chỉnh.
Video hướng dẫn đầy đủ: https://asciinema.org/a/RGwhPeXAPJdhXqxp
Trong video, chúng tôi đã kết nối một mô hình LLM mở (MiniMax-M2.7) với cơ sở dữ liệu dấu vết thông qua MCP (Giao thức ngữ cảnh mô hình):
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonTệp cấu hình JSON cho biết vị trí của máy chủ Ingero và cơ sở dữ liệu dấu vết để tải:
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
Điều này cung cấp cho LLM quyền truy cập trực tiếp vào dữ liệu dấu vết thông qua 7 công cụ: get_trace_stats, get_causal_chains, get_per_process_breakdown, v.v. AI có thể truy vấn cơ sở dữ liệu, tương quan các sự kiện CUDA với dữ liệu lập lịch kernel và tạo ra một giải thích bằng ngôn ngữ đơn giản mà không cần phân tích thủ công.
4 Chuỗi Nguyên Nhân Cấp Độ Cao
Động cơ chuỗi nguyên nhân đã phát hiện ra 4 mẫu cấp độ cao, tất cả đều có cùng nguyên nhân gốc rễ:
[CAO] cudaStreamSync p99=42ms (1.638 lần p50=25us) - CPU 100% + 1.880 sự kiện sched_switch Dòng thời gian: [SYSTEM] CPU 100% [HOST ] 1.880 chuyển đổi ngữ cảnh (21 giây ngoài CPU) [CUDA ] p99=42ms (1.638 lần p50=25us) Nguyên nhân gốc rễ: Công nhân DataLoader tranh giành CPU, áp lực phân bổ trang lớn
[CAO] cudaLaunchKernel p99=24,67ms (349 lần p50=70us) - CPU 100% Nguyên nhân: 34 sự kiện sched_switch [CAO] cuMemAlloc p99=627us (4,0 lần p50) - CPU 100% [CAO] cuLaunchKernel p99=106us (4,0 lần p50) - CPU 100%
cudaStreamSync p99 là 1.638 lần p50. Đó không phải là sự chậm chạp của GPU – đó là GPU chờ đợi dữ liệu không bao giờ đến đúng hạn.
Hình 1: Phân tích được AI tạo ra sau khi chạy /investigate. Mô hình sử dụng 7 công cụ MCP của Ingero để truy vấn cơ sở dữ liệu dấu vết và tạo ra một giải thích bằng ngôn ngữ đơn giản với các khuyến nghị có thể thực hiện được được rút ra trực tiếp từ dữ liệu dấu vết.

Phân Tích Theo Quá Trình
Đây là nơi mọi thứ trở nên rõ ràng. Quá trình chính và 4 công nhân DataLoader của nó được hiển thị như các thực thể riêng biệt:
Quá Trình Chính:
- cudaMemcpyAsync (chuyển giao host-sang-thiết bị): trung bình 301ms, tối đa 2,9 giây - cudaStreamSync: p99 = 42ms (thường 25us) - 1.567 chuyển đổi ngữ cảnh, trung bình 16ms ngoài CPU, sự cố tồi tệ nhất 5 giây - 799.018 phân bổ trang
Công nhân DataLoader 1: 52.863 chuyển đổi ngữ cảnh, 89.338 phân bổ trang, sự cố tồi tệ nhất 5 giây Công nhân DataLoader 2: 50.638 chuyển đổi ngữ cảnh, 83.509 phân bổ trang, sự cố tồi tệ nhất 5 giây Công nhân DataLoader 3: 49.361 chuyển đổi ngữ cảnh, 70.035 phân bổ trang, sự cố tồi tệ nhất 5 giây Công nhân DataLoader 4: 38.862 chuyển đổi ngữ cảnh, 56.354 phân bổ trang, sự cố tồi tệ nhất 5 giây
Tổng cộng trên các công nhân: ~191.000 chuyển đổi ngữ cảnh và ~299.000 phân bổ trang trong 40 giây.
Điều Này Có Nghĩa Là Gì
Các công nhân DataLoader đang thực hiện ba việc tốn kém mà chỉ mục trực tiếp tránh hoàn toàn:
- Trộn và chỉ mục: DataLoader với shuffle=True tạo ra một hoán vị ngẫu nhiên của các chỉ mục, sau đó mỗi công nhân chọn phần của nó. Điều này yêu cầu truy cập bộ nhớ ngẫu nhiên trên toàn bộ tensor 7M mẫu – rất tệ cho tính địa phương của bộ nhớ đệm và kích hoạt lỗi trang.
- Collation và sao chép: Mỗi công nhân thu thập các mẫu phân tán vào một tensor batch liên tục. Điều này có nghĩa là phân bổ bộ nhớ mới (phân bổ trang), sao chép dữ liệu từ các vị trí ngẫu nhiên (lỗi bộ nhớ đệm) và tuần tự hóa kết quả trở lại quá trình chính thông qua bộ nhớ chia sẻ hoặc hàng đợi.
- Cạnh tranh CPU: Bốn công nhân + quá trình chính trên một máy 4-vCPU có nghĩa là việc chiếm dụng liên tục. Mỗi công nhân bị loại bỏ 50.000 lần. Sự cố tồi tệ nhất là 5 giây – trong thời gian đó, GPU không có gì để xử lý.
Với chỉ mục trực tiếp: X[i:i+batch_size] là một view không sao chép của tensor liên tục đã ở trong bộ nhớ. .to(device) kích hoạt một chuyển giao DMA từ một khu vực liên tục đơn. Không có công nhân, không trộn, không collation, không sao chép giữa các quá trình, không chuyển đổi ngữ cảnh. GPU nhận được dữ liệu trong vài micro giây, không phải hàng trăm mili giây.
Giải Pháp
Đối với các công việc GPU trong bộ nhớ nơi toàn bộ tập dữ liệu vừa trong RAM:
- Không sử dụng DataLoader. Chỉ mục trực tiếp với một mảng chỉ mục đã được trộn trước là đơn giản và nhanh hơn 100 lần:
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)
- Nếu bạn phải sử dụng DataLoader, khớp num_workers với số lõi CPU thực tế của bạn trừ 1. Trên một máy 4 lõi, num_workers=2 giảm cạnh tranh. Thêm persistent_workers=True để tránh overhead fork.
- Đối với các tập dữ liệu lớn hơn bộ nhớ nơi DataLoader là cần thiết, nút thắt thực sự chuyển sang I/O đĩa. Sử dụng prefetch_factor=2 (không cao hơn – việc prefetch nhiều hơn có nghĩa là áp lực bộ nhớ nhiều hơn) và đảm bảo rằng lưu trữ của bạn có thể theo kịp.
Tổng Quan Lớn Hơn
Cuộc điều tra này minh họa một mẫu chúng tôi thấy liên tục trong các công việc GPU: GPU nhanh, host là nút thắt, và các chỉ số GPU không thể nhìn thấy nó. nvidia-smi báo cáo sử dụng thấp nhưng không thể giải thích tại sao. torch.profiler đã chụp các kernel CUDA nhưng bỏ lỡ 200.000 chuyển đổi ngữ cảnh xảy ra trong không gian người dùng.
Cách duy nhất để nhìn thấy toàn bộ bức tranh là theo dõi cả hai bên đồng thời – các cuộc gọi API CUDA ở cấp thư viện và các sự kiện lập lịch kernel Linux – và tương quan chúng theo thời gian và ID quá trình. Chuỗi nguyên nhân “CPU 100% -> 1.880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” cho biết câu chuyện hoàn chỉnh trong một dòng. Nếu không có dấu vết chồng chéo, điều này sẽ vẫn là một bí ẩn – như nó đã là đối với người báo cáo ban đầu đã dành nhiều tuần để gỡ lỗi nó.
Hình 2: Khi được hỏi “vấn đề cốt lõi là gì?”, mô hình xác định việc over-subscription CPU gây ra sự chậm trễ lập lịch phía host. cudaLaunchKernel đã chuyển từ 73us sang 25,8ms (356 lần chậm hơn) vì CPU không thể lập lịch khởi động đúng lúc.

Thử Nó
Tái tạo điểm chuẩn:
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()
# Đường dẫn nhanh 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')
# Đường dẫn chậm 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')
Dấu vết với Ingero để xem điều gì đang xảy ra dưới mui:
git clone https://github.com/ingero-io/ingero.git cd ingero && make build sudo ./bin/ingero trace --duration 60s # trong một terminal python3 benchmark.py # trong một terminal khác ./bin/ingero explain --since 60s # sau khi benchmark hoàn thành
Hoặc bỏ qua việc tái tạo và khám phá dữ liệu dấu vết của chúng trực tiếp. Cơ sở dữ liệu điều tra (764KB) nằm trong kho:
# Xem chuỗi nguyên nhân từ cuộc điều tra ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m# Phân tích theo quá trình (xem công nhân DataLoader so với quá trình chính) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m
# Kết nối trợ lý AI của bạn cho điều tra tương tác ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
Điều Tra Với AI (khuyến nghị). Cách nhanh nhất để phân tích dấu vết là kết nối bất kỳ AI tương thích MCP nào trực tiếp với cơ sở dữ liệu. Không cần phân tích thủ công.
Tạo tệp cấu hình:
cat > /tmp/ingero-mcp-dataloader.json << 'EOF'
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
EOF
Sau đó kết nối mô hình của bạn:
# Với Ollama + MiniMax (điều chúng tôi đã sử dụng trong video) ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json# Với Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json
# Với bất kỳ khách hàng MCP nào # Thêm cấu hình trên vào cài đặt MCP của AI
Gõ /investigate để kích hoạt phân tích hướng dẫn, hoặc hỏi bất kỳ câu hỏi nào: “Nguyên nhân gây ra sự đói GPU?” AI có quyền truy cập vào 7 công cụ truy vấn cơ sở dữ liệu dấu vết trực tiếp.
GitHub: github.com/ingero-io/ingero
Vấn Đề Gốc: pytorch/pytorch#154318
Video Hướng Dẫn: https://asciinema.org/a/RGwhPeXAPJdhXqxp
Điều Tra Được Thực Hiện Trên TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.













