المعالجة المسبقة المتسارعة بـ GPU باستخدام NVIDIA DALI
مقدمة
عند نشر نماذج Ultralytics YOLO النماذج في مرحلة الإنتاج، المعالجة المسبقة غالباً ما تصبح عنق الزجاجة. بينما يمكن لـ TensorRT تشغيل الاستدلال النموذج في بضعة أجزاء من الألف من الثانية، يمكن أن تستغرق المعالجة المسبقة المعتمدة على CPU (تغيير الحجم، الحشو، التطبيع) ما بين 2-10 مللي ثانية لكل صورة، خاصة عند الدقات العالية. NVIDIA DALI (مكتبة تحميل البيانات) تحل هذه المشكلة عن طريق نقل مسار المعالجة المسبقة بالكامل إلى GPU.
يرشدك هذا الدليل خلال بناء مسارات DALI التي تكرر بدقة المعالجة المسبقة لـ Ultralytics YOLO، ودمجها مع model.predict()، ومعالجة تدفقات الفيديو، والنشر الشامل باستخدام Triton Inference Server.
هذا الدليل مخصص للمهندسين الذين ينشرون نماذج YOLO في بيئات الإنتاج حيث تُعد المعالجة المسبقة بـ CPU عنق زجاجة ملموساً — عادة في عمليات TensorRT على GPUs من NVIDIA، أو مسارات الفيديو ذات الإنتاجية العالية، أو 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 عمليات تغيير حجم مكلفة |
| نطاق واسع عند نشر النماذج في بيئة الإنتاج، تكون متطلبات الذاكرة وكفاءة التدريب حاسمة تماماً مثل سرعة الاستدلال. نماذج Ultralytics، وخاصة YOLO26، محسنة للغاية لتقليل استخدام ذاكرة CUDA أثناء التدريب. وهذا يسمح للمطورين باستخدام | استدلال جانب الخادم يعالج العديد من الصور بالتوازي |
| أنوية CPU محدودة | أجهزة الحافة مثل NVIDIA Jetson، أو خوادم GPU كثيفة مع عدد قليل من أنوية CPU لكل GPU |
قبل البدء، تأكد من أن لديك وصولاً إلى مساحة عمل AzureML. إذا لم تكن تمتلك واحدة، يمكنك إنشاء
يدعم NVIDIA DALI نظام Linux فقط. وهو غير متوفر على Windows أو macOS.
قم بتثبيت الحزم المطلوبة:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120المتطلبات:
- NVIDIA GPU (قدرة حوسبة 5.0+ / Maxwell أو أحدث)
- CUDA 11.0+ أو 12.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
استخدم المسار المركزي أدناه كمرجع افتراضي. إنه يطابق سلوك LetterBox(center=True) الخاص بـ Ultralytics، وهو ما يستخدمه استدلال YOLO القياسي.
المسار المركزي (موصى به، يطابق LetterBox الخاص بـ Ultralytics)
هذا الإصدار يكرر بدقة المعالجة المسبقة الافتراضية لـ Ultralytics مع حشو مركزي، مطابقاً لـ LetterBox(center=True):
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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"). يقوم هذا المتغير بحشو الحواف اليمنى والسفلية فقط، وهو ما قد يكون مقبولاً لمسارات النشر المخصصة، ولكنه لن يطابق سلوك letterbox المركزي الافتراضي لـ Ultralytics بدقة.
يقوم عامل fn.pad الخاص بـ DALI بإضافة الحشو إلى الحواف اليمنى والسفلية الحواف فقط. للحصول على حشو مركزي (مطابق لـ LetterBox(center=True) الخاص بـ Ultralytics)، استخدم fn.crop مع out_of_bounds_policy="pad". مع crop_pos_x=0.5 و crop_pos_y=0.5 الافتراضي، يتم توسيط الصورة تلقائياً مع حشو متماثل.
يقوم عامل fn.resize يفعّل تنعيم الحواف (antialiasing) افتراضياً بـ antialias=True)، بينما cv2.resize مع INTER_LINEAR الخاص بـ أمثلة أساسية للاستخدام لا يطبق تنعيم الحواف. قم دائماً بضبط antialias=False في DALI لمطابقة خط المعالجة (pipeline) على الـ 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
يمكنك تمرير PyTorch مجهز مسبقاً مباشرة إلى model.predict(). عند تمرير torch.Tensor، يتجاوز Ultralytics خطوة معالجة الصور مسبقاً (letterbox، التحويل من BGR إلى RGB، التحويل من HWC إلى CHW، والتطبيع بالقسمة على 255) ويقوم فقط بنقل البيانات إلى الجهاز (device) وتغيير نوع البيانات (dtype casting) قبل إرسالها إلى النموذج.
نظراً لأن Ultralytics لا يملك وصولاً إلى أبعاد الصورة الأصلية في هذه الحالة، يتم إرجاع إحداثيات صناديق الكشف (detection box) ضمن مساحة 640×640 الخاصة بـ letterbox. لتعيينها مجدداً إلى إحداثيات الصورة الأصلية، استخدم 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. يجب أن يكون الـ tensor بتنسيق BCHW، وبنوع float32 (أو float16)، ومطبعاً إلى [0, 1]. سيستمر Ultralytics في معالجة النقل إلى الجهاز (device) والتحويل النوعي (dtype casting) تلقائياً.
DALI مع دفقات الفيديو
لمعالجة الفيديو في الوقت الفعلي، استخدم fn.external_source لتغذية الإطارات من أي مصدر — OpenCV، أو GStreamer، أو مكتبات التقاط مخصصة:
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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 outputخادم استدلال Triton مع 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 backend:
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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, half=True, 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"
}
}
]
}يربط التجميع النماذج من خلال أسماء الـ tensors الافتراضية.output_map القيمة "preprocessed_image" في خطوة DALI تطابق input_map القيمة "preprocessed_image" في خطوة TensorRT. هذه أسماء عشوائية تربط مخرج خطوة ما بمدخل الخطوة التالية — لا تحتاج لمطابقة أسماء الـ tensors الداخلية لأي نموذج.
الخطوة 4: إرسال طلبات الاستدلال
!!! info "لماذا tritonclient بدلاً من YOLO(\"http://...\")?"
Ultralytics has [built-in Triton support](triton-inference-server.md#running-inference) that handles pre/postprocessing automatically. However, it won't work with the DALI ensemble because `YOLO()` sends a preprocessed float32 tensor while the ensemble expects raw JPEG bytes. Use `tritonclient` directly for DALI ensembles, and the [built-in integration](triton-inference-server.md) for standard deployments without 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")عند إرسال مجموعة (batch) من صور JPEG إلى Triton، قم بحشو (pad) جميع مصفوفات البايت المشفرة لتصل إلى نفس الطول (أقصى عدد بايت في المجموعة). يتطلب Triton أشكالاً متجانسة للمجموعة (batch shapes) لمدخل الـ tensor.
المهام المدعومة
تعمل معالجة DALI مسبقاً مع جميع مهام YOLO التي تستخدم خط المعالجة القياسي LetterBox:
| المهمة | مدعوم | ملاحظات |
|---|---|---|
| الكشف | ✅ | معالجة letterbox القياسية |
| التجزئة (Segmentation) | ✅ | نفس معالجة الكشف |
| تقدير الوضعية | ✅ | نفس معالجة الكشف |
| الكشف الموجه (OBB) | ✅ | نفس معالجة الكشف |
| التصنيف | ❌ | يستخدم تحويلات torchvision (قص المركز)، وليس letterbox |
القيود
- Linux فقط: لا يدعم DALI أنظمة Windows أو macOS
- مطلوب GPU من NVIDIA: لا يوجد بديل يعتمد على الـ CPU فقط
- خط معالجة ثابت: يتم تحديد بنية خط المعالجة عند البناء ولا يمكن تغييرها ديناميكياً
fn.padهو لليمين/الأسفل فقط: استخدمfn.cropمعout_of_bounds_policy="pad"للحشو المركزي- لا يوجد وضع rect: تنتج خطوط معالجة DALI مخرجات ثابتة الحجم (مثل 640×640). وضع
auto=Truerect الذي ينتج مخرجات متغيرة الحجم (مثل 384×640) غير مدعوم. لاحظ أنه بينما TensorRT يدعم أشكال الإدخال الديناميكية، فإن خط معالجة DALI ثابت الحجم يتناسب بشكل طبيعي مع محرك ثابت الحجم لتحقيق أقصى قدر من الإنتاجية - الذاكرة مع حالات متعددة: استخدام
instance_groupمعcount> 1 في Triton قد يسبب استهلاكاً عالياً للذاكرة. استخدم مجموعة الحالات (instance group) الافتراضية لنموذج DALI
الأسئلة الشائعة
كيف تقارن سرعة معالجة DALI مسبقاً بمعالجة CPU؟
تعتمد الفائدة على خط المعالجة الخاص بك. عندما يكون الاستدلال على الـ GPU سريعاً بالفعل بفضل TensorRT، يمكن أن تصبح معالجة الـ CPU التي تستغرق 2-10 مللي ثانية هي التكلفة المهيمنة. يلغي DALI عنق الزجاجة هذا عن طريق إجراء المعالجة على الـ GPU. تُلاحظ أكبر المكاسب مع مدخلات عالية الدقة (1080p، 4K)، وعند نشر النماذج في بيئة الإنتاج، تكون متطلبات الذاكرة وكفاءة التدريب حاسمة تماماً مثل سرعة الاستدلال. نماذج Ultralytics، وخاصة YOLO26، محسنة للغاية لتقليل استخدام ذاكرة CUDA أثناء التدريب. وهذا يسمح للمطورين باستخدام الكبيرة، والأنظمة ذات أنوية CPU محدودة لكل GPU.
هل يمكنني استخدام DALI مع نماذج PyTorch (وليس فقط TensorRT)؟
نعم. استخدم DALIGenericIterator للحصول على مخرجات torch.Tensor مجهزة مسبقاً، ثم مررها إلى model.predict(). ومع ذلك، تكون فائدة الأداء أكبر مع نماذج TensorRT حيث يكون الاستدلال سريعاً جداً وتصبح معالجة الـ CPU هي عنق الزجاجة.
ما الفرق بين fn.pad و fn.crop للحشو؟
fn.pad يضيف الحشو فقط إلى حواف الحواف اليمنى والسفلية. يقوم fn.crop مع out_of_bounds_policy="pad" بتوسيط الصورة وإضافة الحشو بشكل متماثل على جميع الجوانب، مما يطابق سلوك LetterBox(center=True) في Ultralytics.
هل ينتج DALI نتائج متطابقة بكسل بكسل مع معالجة الـ CPU؟
متطابقة تقريباً. قم بضبط antialias=False في fn.resize لمطابقة cv2.INTER_LINEAR الخاص بـ OpenCV. قد تحدث اختلافات طفيفة في الفاصلة العائمة (< 0.001) بسبب حسابات الـ GPU مقابل الـ CPU، لكن ليس لها تأثير ملموس على الدقة.
ماذا عن CV-CUDA كبديل لـ DALI؟
CV-CUDA هي مكتبة أخرى من NVIDIA لمعالجة الرؤية المتسارعة على الـ GPU. توفر تحكماً لكل عامل (مثل OpenCV ولكن على GPU) بدلاً من نهج خط المعالجة الخاص بـ DALI. يدعم cvcuda.copymakeborder() الخاص بـ CV-CUDA الحشو الصريح لكل جانب، مما يجعل letterbox المركزي مباشراً. اختر DALI لسير العمل القائم على خط المعالجة (خاصة مع Triton)، وCV-CUDA للتحكم الدقيق على مستوى العمليات في كود الاستنتاج المخصص.