معالجة أولية متسارعة بواسطة GPU باستخدام NVIDIA DALI#
عند نشر نماذج Ultralytics YOLO في بيئات الإنتاج، غالبًا ما يصبح المعالجة المسبقة هي العامل المحدود للأداء. بينما يمكن لـ TensorRT تشغيل الاستدلال للنموذج في بضعة مللي ثانية فقط، فإن المعالجة المسبقة القائمة على وحدة المعالجة المركزية CPU (مثل تغيير الحجم، والحشو، والتطبيع) قد تستغرق من 2 إلى 10 مللي ثانية لكل صورة، خاصة عند الدقة العالية. تعمل مكتبة NVIDIA DALI (مكتبة تحميل البيانات) على حل هذه المشكلة عن طريق نقل خط أنابيب المعالجة المسبقة بالكامل إلى وحدة معالجة الرسومات GPU.
يرشدك هذا الدليل خلال إنشاء خطوط أنابيب DALI التي تكرر تمامًا المعالجة المسبقة لـ Ultralytics YOLO، ودمجها مع model.predict()، ومعالجة تدفقات الفيديو، والنشر من البداية إلى النهاية باستخدام Triton Inference Server.
هذا الدليل مخصص للمهندسين الذين ينشرون نماذج YOLO في بيئات الإنتاج حيث تكون المعالجة المسبقة لوحدة المعالجة المركزية CPU عنق زجاجة مقاسًا - وعادة ما يكون ذلك في عمليات نشر TensorRT على وحدات معالجة الرسومات NVIDIA GPU، أو خطوط أنابيب الفيديو ذات الإنتاجية العالية، أو إعدادات Triton Inference Server. إذا كنت تقوم بتشغيل الاستدلال القياسي باستخدام model.predict() ولم يكن لديك عنق زجاجة في المعالجة المسبقة، فإن خط الأنابيب الافتراضي لوحدة المعالجة المركزية CPU يعمل بشكل جيد.
- هل تقوم بنسخ خط أنابيب DALI؟ استخدم
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeلتكرار المعالجة المسبقة بنمط الرسالة (letterbox) الخاصة بـ YOLO على وحدة معالجة الرسومات GPU. - هل تريد التكامل مع Ultralytics؟ قم تمرير مخرج DALI كـ
torch.Tensorإلىmodel.predict()- حيث تتخطى Ultralytics المعالجة المسبقة للصورة تلقائيًا. - هل تنشر باستخدام Triton؟ استخدم واجهة DALI الخلفية مع مجموعة TensorRT للحصول على معالجة أولية لا تعتمد على CPU على الإطلاق.
لماذا تستخدم DALI لمعالجة YOLO الأولية؟#
في خط استدلال YOLO التقليدي، يتم تشغيل خطوات المعالجة الأولية على CPU:
- فك ترميز الصورة (JPEG/PNG)
- تغيير الحجم مع الحفاظ على نسبة العرض إلى الارتفاع
- الحشو إلى الحجم المستهدف (letterbox)
- تطبيع قيم البكسل من
[0, 255]إلى[0, 1] - تحويل التنسيق من HWC إلى CHW
مع DALI، يتم تشغيل كل هذه العمليات على GPU، مما يلغي عنق زجاجة CPU. وهذا ذو قيمة خاصة عند:
| السيناريو | لماذا تساعد DALI |
|---|---|
| استدلال سريع بواسطة GPU | محركات TensorRT ذات الاستدلال الذي يستغرق أقل من مللي ثانية تجعل المعالجة المسبقة لوحدة المعالجة المركزية CPU التكلفة المهيمنة |
| المدخلات عالية الدقة | تتطلب تدفقات الفيديو بدقة 1080p و 4K عمليات تغيير حجم مكلفة |
| أحجام الدفعات الكبيرة | استدلال من جانب الخادم يعالج العديد من الصور بالتوازي |
| محدودية أنوية CPU | أجهزة الحافة مثل NVIDIA Jetson، أو خوادم وحدات معالجة الرسومات الكثيفة التي تحتوي على عدد قليل من أنوية وحدة المعالجة المركزية لكل وحدة معالجة رسومات |
المتطلبات الأساسية#
تدعم NVIDIA DALI نظام Linux فقط. وهي غير متوفرة على Windows أو macOS.
قم بتثبيت الحزم المطلوبة:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130المتطلبات:
- NVIDIA GPU (قدرة حوسبة 5.0+ / Maxwell أو أحدث)
- CUDA 11.0+, 12.0+ أو 13.0+
- Python 3.10-3.14
- نظام التشغيل Linux
فهم المعالجة الأولية لـ YOLO#
قبل بناء خط أنابيب DALI، يجدر فهم ما تقوم به Ultralytics تمامًا أثناء المعالجة المسبقة. الفئة الأساسية هي LetterBox في ultralytics/data/augment.py:
from ultralytics.data.augment import LetterBox
letterbox = LetterBox(
new_shape=(640, 640), # Target size
center=True, # Center the image (pad equally on both sides)
stride=32, # Stride alignment
padding_value=114, # Gray padding (114, 114, 114)
)يقوم خط أنابيب المعالجة المسبقة الكامل في ultralytics/engine/predictor.py بهذه الخطوات:
| الخطوة | العملية | وظيفة CPU | ما يعادلها في DALI |
|---|---|---|---|
| 1 | تغيير الحجم بنظام Letterbox | cv2.resize | fn.resize(mode="not_larger") |
| 2 | حشو مركزي | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + تطبيع /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
تحافظ عملية letterbox على نسبة العرض إلى الارتفاع من خلال:
- حساب مقياس التحجيم:
r = min(target_h / h, target_w / w) - تغيير الحجم إلى
(round(w * r), round(h * r)) - حشو المساحة المتبقية باللون الرمادي (
114) للوصول إلى الحجم المستهدف - توسيط الصورة بحيث يتم توزيع الحشو بالتساوي على كلا الجانبين
خط معالجة DALI لـ YOLO#
يكرر خط أنابيب DALI الموصى به سلوك LetterBox(center=True) الافتراضي لـ Ultralytics، وهو ما يستخدمه استدلال YOLO القياسي.
خط معالجة مركزي (موصى به، يطابق Ultralytics LetterBox)#
يكرر هذا الإصدار بدقة المعالجة المسبقة الافتراضية لـ Ultralytics مع الحشو المتمركز، مطابقًا LetterBox(center=True):
from nvidia import dali
from nvidia.dali import fn, types
@dali.pipeline_def(batch_size=8, num_threads=4, device_id=0)
def yolo_dali_pipeline_centered(image_dir, target_size=640):
"""DALI pipeline replicating YOLO preprocessing with centered padding.
Matches Ultralytics LetterBox(center=True) behavior exactly.
"""
# Read and decode images on GPU
jpegs, _ = fn.readers.file(file_root=image_dir, random_shuffle=False, name="Reader")
images = fn.decoders.image(jpegs, device="mixed", output_type=types.RGB)
# Aspect-ratio-preserving resize
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # Match cv2.INTER_LINEAR (no antialiasing)
)
# Centered padding using fn.crop with out_of_bounds_policy
# When crop size > image size, fn.crop centers the image and pads symmetrically
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # YOLO padding value
)
# Normalize and convert layout
output = fn.crop_mirror_normalize(
padded,
dtype=types.FLOAT,
output_layout="CHW",
mean=[0.0, 0.0, 0.0],
std=[255.0, 255.0, 255.0],
)
return outputإذا لم تكن بحاجة إلى تطابق دقيق مع LetterBox(center=True)، يمكنك تبسيط خطوة الحشو باستخدام fn.pad(...) بدلاً من fn.crop(..., out_of_bounds_policy="pad"). يقوم هذا المتغير بحشو حواف اليمين والأسفل فقط، وهو ما قد يكون مقبولاً لخطوط أنابيب النشر المخصصة، ولكنه لن يتطابق تمامًا مع سلوك الرسالة المركزية الافتراضي لـ Ultralytics.
يضيف مشغل fn.pad في DALI الحشو إلى حواف اليمين والأسفل فقط. للحصول على حشو متمركز (مطابق لـ Ultralytics LetterBox(center=True))، استخدم fn.crop مع out_of_bounds_policy="pad". مع crop_pos_x=0.5 و crop_pos_y=0.5 الافتراضيين، يتم توسيط الصورة تلقائيًا مع حشو متماثل.
يُمكّن عامل fn.resize في DALI تنعيم الحواف (antialiasing) افتراضيًا (antialias=True)، بينما لا يقوم عامل OpenCV cv2.resize مع INTER_LINEAR بتطبيق تنعيم الحواف. احرص دائمًا على تعيين antialias=False في DALI ليتطابق مع خط أنابيب وحدة المعالجة المركزية CPU. يؤدي إغفال ذلك إلى اختلافات عددية طفيفة يمكن أن تؤثر على دقة النموذج.
تشغيل خط المعالجة#
# Build and run the pipeline
pipe = yolo_dali_pipeline_centered(image_dir="/path/to/images", target_size=640)
pipe.build()
# Get a batch of preprocessed images
(output,) = pipe.run()
# Convert to numpy or PyTorch tensors
batch_np = output.as_cpu().as_array() # Shape: (batch_size, 3, 640, 640)
print(f"Output shape: {batch_np.shape}, dtype: {batch_np.dtype}")
print(f"Value range: [{batch_np.min():.4f}, {batch_np.max():.4f}]")استخدام DALI مع Ultralytics Predict#
يمكنك تمرير موتر PyTorch مُعالج مسبقًا مباشرة إلى model.predict(). عند تمرير torch.Tensor، تتخطى Ultralytics المعالجة المسبقة للصورة (تغيير الحجم، BGR→RGB، HWC→CHW، والتطبيع /255) وتجري فقط نقل الجهاز وتحويل نوع البيانات (dtype) قبل إرساله إلى النموذج.
نظرًا لأن Ultralytics لا تملك إمكانية الوصول إلى أبعاد الصورة الأصلية في هذه الحالة، يتم إرجاع إحداثيات صناديق الكشف في مساحة الرسالة 640×640. لرسم خرائط لها مرة أخرى إلى إحداثيات الصورة الأصلية، استخدم scale_boxes الذي يتعامل مع منطق التقريب الدقيق الذي تستخدمه LetterBox:
from ultralytics.utils.ops import scale_boxes
# boxes: tensor of shape (N, 4) in xyxy format, in 640x640 letterboxed coords
# Scale boxes from letterboxed (640, 640) back to original (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))ينطبق هذا على جميع مسارات المعالجة الأولية الخارجية — إدخال tensor المباشر، وتدفقات الفيديو، ونشر Triton.
from nvidia.dali.plugin.pytorch import DALIGenericIterator
from ultralytics import YOLO
# Load model
model = YOLO("yolo26n.pt")
# Create DALI iterator
pipe = yolo_dali_pipeline_centered(image_dir="/path/to/images", target_size=640)
pipe.build()
dali_iter = DALIGenericIterator(pipe, ["images"], reader_name="Reader")
# Run inference with DALI-preprocessed tensors
for batch in dali_iter:
images = batch[0]["images"] # Already on GPU, shape (B, 3, 640, 640)
results = model.predict(images, verbose=False)
for result in results:
print(f"Detected {len(result.boxes)} objects")عندما تقوم بتمرير torch.Tensor إلى model.predict()، تستغرق خطوة المعالجة المسبقة للصورة ~0.004 مللي ثانية (تكاد تكون معدومة) مقارنة بـ ~1-10 مللي ثانية مع المعالجة المسبقة لوحدة المعالجة المركزية CPU. يجب أن يكون الموتر بتنسيق BCHW، ونوع float32 (أو float16)، ومطبعًا إلى [0, 1]. ستظل Ultralytics تتعامل مع نقل الجهاز وتحويل نوع البيانات تلقائيًا.
DALI مع تدفقات الفيديو#
لمعالجة الفيديو في الوقت الفعلي، استخدم fn.external_source لتغذية الإطارات من أي مصدر — OpenCV، أو GStreamer، أو مكتبات التقاط مخصصة:
from nvidia import dali
from nvidia.dali import fn, types
@dali.pipeline_def(batch_size=1, num_threads=4, device_id=0)
def yolo_video_pipeline(target_size=640):
"""DALI pipeline for processing video frames from external source."""
# External source for feeding frames from OpenCV, GStreamer, etc.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Move to GPU and preprocess
frames_gpu = frames.gpu()
resized = fn.resize(
frames_gpu,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False,
)
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114,
)
output = fn.crop_mirror_normalize(
padded,
dtype=types.FLOAT,
output_layout="CHW",
mean=[0.0, 0.0, 0.0],
std=[255.0, 255.0, 255.0],
)
return outputTriton Inference Server مع DALI#
لنشر الإنتاج، ادمج المعالجة المسبقة لـ DALI مع استدلال TensorRT في Triton Inference Server باستخدام نموذج جماعي (ensemble). هذا يلغي المعالجة المسبقة لوحدة المعالجة المركزية CPU تمامًا - حيث تدخل بيانات JPEG الخام وتخرج عمليات الكشف، مع معالجة كل شيء على وحدة معالجة الرسومات GPU.
هيكل مستودع النموذج#
model_repository/
├── dali_preprocessing/
│ ├── 1/
│ │ └── model.dali
│ └── config.pbtxt
├── yolo_trt/
│ ├── 1/
│ │ └── model.plan
│ └── config.pbtxt
└── ensemble_dali_yolo/
├── 1/ # Empty directory (required by Triton)
└── config.pbtxtالخطوة 1: إنشاء خط معالجة DALI#
تسلسل خط معالجة DALI لخدمة Triton DALI:
from nvidia import dali
from nvidia.dali import fn, types
@dali.pipeline_def(batch_size=8, num_threads=4, device_id=0)
def triton_dali_pipeline():
"""DALI preprocessing pipeline for Triton deployment."""
# Input: raw encoded image bytes from Triton
images = fn.external_source(device="cpu", name="DALI_INPUT_0")
images = fn.decoders.image(images, device="mixed", output_type=types.RGB)
resized = fn.resize(
images,
resize_x=640,
resize_y=640,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False,
)
padded = fn.crop(
resized,
crop=(640, 640),
out_of_bounds_policy="pad",
fill_values=114,
)
output = fn.crop_mirror_normalize(
padded,
dtype=types.FLOAT,
output_layout="CHW",
mean=[0.0, 0.0, 0.0],
std=[255.0, 255.0, 255.0],
)
return output
# Serialize pipeline to model repository
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")الخطوة 2: تصدير YOLO إلى TensorRT#
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.export(format="engine", imgsz=640, quantize=16, batch=8)
# Copy the .engine file to model_repository/yolo_trt/1/model.planالخطوة 3: تكوين Triton#
dali_preprocessing/config.pbtxt:
name: "dali_preprocessing"
backend: "dali"
max_batch_size: 8
input [
{
name: "DALI_INPUT_0"
data_type: TYPE_UINT8
dims: [ -1 ]
}
]
output [
{
name: "DALI_OUTPUT_0"
data_type: TYPE_FP32
dims: [ 3, 640, 640 ]
}
]yolo_trt/config.pbtxt:
name: "yolo_trt"
platform: "tensorrt_plan"
max_batch_size: 8
input [
{
name: "images"
data_type: TYPE_FP32
dims: [ 3, 640, 640 ]
}
]
output [
{
name: "output0"
data_type: TYPE_FP32
dims: [ 300, 6 ]
}
]ensemble_dali_yolo/config.pbtxt:
name: "ensemble_dali_yolo"
platform: "ensemble"
max_batch_size: 8
input [
{
name: "INPUT"
data_type: TYPE_UINT8
dims: [ -1 ]
}
]
output [
{
name: "OUTPUT"
data_type: TYPE_FP32
dims: [ 300, 6 ]
}
]
ensemble_scheduling {
step [
{
model_name: "dali_preprocessing"
model_version: -1
input_map {
key: "DALI_INPUT_0"
value: "INPUT"
}
output_map {
key: "DALI_OUTPUT_0"
value: "preprocessed_image"
}
},
{
model_name: "yolo_trt"
model_version: -1
input_map {
key: "images"
value: "preprocessed_image"
}
output_map {
key: "output0"
value: "OUTPUT"
}
}
]
}يربط النموذج الجماعي النماذج من خلال أسماء الموترات الافتراضية. تتطابق قيمة output_map المتمثلة في "preprocessed_image" في خطوة DALI مع قيمة input_map المتمثلة في "preprocessed_image" في خطوة TensorRT. هذه أسماء تعسفية تربط مخرج خطوة ما بمدخل الخطوة التالية - ولا يلزم أن تتطابق مع أي من أسماء الموترات الداخلية للنموذج.
الخطوة 4: إرسال طلبات الاستدلال#
تحتوي Ultralytics على دعم مدمج لـ Triton يتعامل مع المعالجة المسبقة واللاحقة تلقائيًا. ومع ذلك، لن يعمل مع مجموعة DALI لأن YOLO() يرسل موتر float32 معالج مسبقًا بينما يتوقع النموذج الجماعي بيانات JPEG خام. استخدم tritonclient مباشرة لمجموعات DALI، والتكامل المدمج لعمليات النشر القياسية بدون DALI.
import numpy as np
import tritonclient.http as httpclient
client = httpclient.InferenceServerClient(url="localhost:8000")
# Load image as raw bytes (JPEG/PNG encoded)
image_data = np.fromfile("image.jpg", dtype="uint8")
image_data = np.expand_dims(image_data, axis=0) # Add batch dimension
# Create input
input_tensor = httpclient.InferInput("INPUT", image_data.shape, "UINT8")
input_tensor.set_data_from_numpy(image_data)
# Run inference through the ensemble
result = client.infer(model_name="ensemble_dali_yolo", inputs=[input_tensor])
detections = result.as_numpy("OUTPUT") # Shape: (1, 300, 6) -> [x1, y1, x2, y2, conf, class_id]
# Filter by confidence (no NMS needed — YOLO26 is end-to-end)
detections = detections[0] # First image
detections = detections[detections[:, 4] > 0.25] # Confidence threshold
print(f"Detected {len(detections)} objects")عند إرسال دفعة من صور JPEG إلى Triton، قم بحشو جميع مصفوفات البايتات المشفرة لتصل إلى نفس الطول (الحد الأقصى لعدد البايتات في الدفعة). يتطلب Triton أشكال دفعات متجانسة لموتر الإدخال.
المهام المدعومة#
تعمل المعالجة المسبقة لـ DALI مع جميع مهام YOLO التي تستخدم خط أنابيب LetterBox القياسي:
| المهمة | مدعوم | ملاحظات |
|---|---|---|
| الاكتشاف | ✅ | معالجة Letterbox القياسية |
| تجزئة الحالات الفردية | ✅ | نفس المعالجة المسبقة المستخدمة في الكشف |
| التجزئة الدلالية | ✅ | نفس معالجة الصور المسبقة المستخدمة في الكشف |
| التصنيف | ❌ | يستخدم تحويلات torchvision (القص من المنتصف)، وليس Letterbox |
| تقدير الوضعية | ✅ | نفس المعالجة المسبقة المستخدمة في الكشف |
| الكشف الموجه (OBB) | ✅ | نفس المعالجة المسبقة المستخدمة في الكشف |
القيود#
- Linux فقط: لا يدعم DALI أنظمة Windows أو macOS
- مطلوب NVIDIA GPU: لا يوجد خيار احتياطي يعتمد على CPU فقط
- خط أنابيب ثابت: يتم تحديد هيكل خط الأنابيب في وقت الإنشاء ولا يمكن تغييره ديناميكيًا
fn.padلليمين والأسفل فقط: استخدمfn.cropمعout_of_bounds_policy="pad"للحصول على حشو متمركز- لا يوجد وضع مستطيل (rect mode): تنتج خطوط أنابيب DALI مخرجًا ثابت الحجم (مثل 640×640). وضع المستطيل
auto=Trueالذي ينتج مخرجات متغيرة الحجم (مثل 384×640) غير مدعوم. لاحظ أنه بينما يدعم TensorRT أشكال الإدخال الديناميكية، فإن خط أنابيب DALI ثابت الحجم يتزاوج بشكل طبيعي مع محرك ثابت الحجم لتحقيق أقصى إنتاجية. - الذاكرة مع مثيلات متعددة: استخدام
instance_groupمعcount> 1 في Triton يمكن أن يتسبب في استهلاك عالٍ للذاكرة. استخدم مجموعة المثيلات الافتراضية لنموذج DALI
الأسئلة الشائعة#
تعتمد الفائدة على خط الأنابيب الخاص بك. عندما يكون استدلال وحدة معالجة الرسومات GPU سريعًا بالفعل باستخدام TensorRT، يمكن أن تصبح المعالجة المسبقة لوحدة المعالجة المركزية CPU التي تستغرق 2-10 مللي ثانية هي التكلفة المهيمنة. تزيل DALI هذا العنق الزجاجي عن طريق تشغيل المعالجة المسبقة على وحدة معالجة الرسومات GPU. تُرى أكبر المكاسب مع المدخلات عالية الدقة (1080p، 4K)، وأحجام الدفعات الكبيرة، والأنظمة ذات أنوية وحدة المعالجة المركزية المحدودة لكل وحدة معالجة رسومات.
نعم. استخدم
DALIGenericIteratorللحصول على مخرجاتtorch.Tensorمعالجة مسبقًا، ثم قم بتمريرها إلىmodel.predict(). ومع ذلك، تكون فائدة الأداء أكبر ما يمكن مع نماذج TensorRT حيث يكون الاستدلال سريعًا للغاية بالفعل وتصبح المعالجة المسبقة لوحدة المعالجة المركزية CPU هي عنق الزجاجة.يضيف
fn.padالحشو إلى حواف اليمين والأسفل فقط. يقومfn.cropمعout_of_bounds_policy="pad"بتوسيط الصورة وإضافة الحشو بشكل متماثل على جميع الجوانب، مطابقًا سلوك UltralyticsLetterBox(center=True).متطابقان تقريبا. قم بتعيين
antialias=Falseفيfn.resizeليتطابق معcv2.INTER_LINEARالخاص بـ OpenCV. قد تحدث اختلافات طفيفة في الفاصلة العائمة (< 0.001) بسبب حسابات وحدة معالجة الرسومات مقابل وحدة المعالجة المركزية، ولكن ليس لها أي تأثير قابل للقياس على دقة الكشف.CV-CUDA هي مكتبة أخرى من NVIDIA لمعالجة الرؤية المتسارعة بواسطة وحدة معالجة الرسومات GPU. توفر تحكمًا لكل عامل (مثل OpenCV ولكن على وحدة معالجة الرسومات GPU) بدلاً من نهج خط الأنابيب الخاص بـ DALI. يدعم
cvcuda.copymakeborder()في CV-CUDA الحشو الصريح لكل جانب، مما يجعل الرسالة المركزية أمرًا سهلاً. اختر DALI لمهام العمل القائمة على خطوط الأنابيب (خاصة مع Triton)، وCV-CUDA للتحكم الدقيق على مستوى العامل في رمز الاستدلال المخصص.