المعالجة المسبقة المعززة بوحدة GPU باستخدام NVIDIA DALI#
عند نشر نماذج Ultralytics YOLO في بيئة الإنتاج، غالباً ما تصبح المعالجة المسبقة عنق الزجاجة. في حين يمكن لـTensorRT تنفيذ استدلال النموذج في بضعة مللي ثوانٍ فقط، قد تستغرق المعالجة المسبقة المعتمدة على CPU (تغيير الحجم، وإضافة الحشو، والتطبيع) من 2 إلى 10 مللي ثوانٍ لكل صورة، خصوصاً عند الدقات العالية. وتعالج NVIDIA DALI (مكتبة تحميل البيانات) هذه المشكلة بنقل خط أنابيب المعالجة المسبقة بأكمله إلى GPU.
يرشدك هذا الدليل إلى بناء خطوط أنابيب DALI التي تكرر بدقة المعالجة المسبقة بأسلوب letterbox في Ultralytics YOLO، ودمجها مع model.predict()، ومعالجة تدفقات الفيديو، ونشرها من البداية إلى النهاية باستخدام Triton Inference Server.
هذا الدليل موجّه إلى المهندسين الذين ينشرون نماذج YOLO في بيئات الإنتاج التي تشكّل فيها المعالجة المسبقة على CPU عنق زجاجة مثبتاً بالقياس — ويشمل ذلك عادةً عمليات النشر باستخدام TensorRT على وحدات GPU من 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. - هل تدمج DALI مع 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، أو خوادم GPU عالية الكثافة التي تضم عددًا قليلًا من أنوية CPU لكل GPU |
المتطلبات الأساسية#
يدعم NVIDIA DALI نظام Linux فقط. وهو غير متاح على Windows أو macOS.
ثبّت الحزم المطلوبة:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130المتطلبات:
- GPU من NVIDIA (إصدار قدرة حوسبية 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), # حجم الهدف
center=True, # توسيط الصورة (إضافة حشو متساوٍ على الجانبين)
stride=32, # محاذاة الخطوة
padding_value=114, # حشو رمادي (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 القياسي.
المسار المركزي (موصى به، يطابق LetterBox في Ultralytics)#
يحاكي هذا الإصدار بدقة المعالجة المسبقة الافتراضية في 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.
"""
# قراءة الصور وفك ترميزها على 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)
# تغيير الحجم مع الحفاظ على نسبة العرض إلى الارتفاع
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # مطابقة cv2.INTER_LINEAR (دون تنعيم الحواف)
)
# حشو مركزي باستخدام fn.crop مع out_of_bounds_policy
# عندما يكون حجم القص أكبر من حجم الصورة، توسيط fn.crop الصورة وإضافة حشو متماثل
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # قيمة حشو YOLO
)
# تطبيع البيانات وتحويل التخطيط
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 في DALI تنعيم الحواف افتراضيًا (antialias=True)، بينما لا يطبّق cv2.resize في OpenCV مع 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 مع Predict في Ultralytics#
يمكنك تمرير موتر PyTorch خضع للمعالجة المسبقة مباشرةً إلى model.predict(). عند تمرير torch.Tensor، تتجاوز Ultralytics المعالجة المسبقة للصورة (letterbox وBGR→RGB وHWC→CHW والتطبيع /255)، ولا تنفّذ سوى نقل الجهاز وتحويل نوع البيانات قبل إرسالها إلى النموذج.
بما أن Ultralytics لا تملك أبعاد الصورة الأصلية في هذه الحالة، تُعاد إحداثيات مربعات الكشف ضمن مساحة letterbox ذات الأبعاد 640×640. لإعادتها إلى إحداثيات الصورة الأصلية، استخدم scale_boxes، الذي يتعامل مع منطق التقريب الدقيق المستخدم في LetterBox:
from ultralytics.utils.ops import scale_boxes
# مربعات: موتر بالشكل (N, 4) بتنسيق xyxy، وإحداثياتها ضمن مساحة letterbox 640x640
# تحجيم المربعات من مساحة letterbox (640, 640) إلى أبعاد الصورة الأصلية (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))ينطبق ذلك على جميع مسارات المعالجة المسبقة الخارجية — إدخال الموتر مباشرةً، وتدفقات الفيديو، والنشر باستخدام 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.004ms (أي لا تكاد تستغرق وقتًا)، مقارنةً بنحو ~1-10ms عند إجراء المعالجة المسبقة على 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."""
# مصدر خارجي لإدخال الإطارات من OpenCV وGStreamer وغيرهما.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# النقل إلى GPU وإجراء المعالجة المسبقة
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، باستخدام نموذج تجميعي. يلغي ذلك المعالجة المسبقة على 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 لاستخدامه مع خلفية DALI في Triton:
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."""
# الإدخال: بايتات الصورة الخام المشفّرة من 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
# تسلسل المسار في مستودع النماذج
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")الخطوة 2: تصدير YOLO إلى TensorRT#
from pathlib import Path
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
engine_path = model.export(
format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
) # من دون NMS (N, 300, 6)؛ TensorRT >= 8.5
# تضيف Ultralytics ترويسة بيانات وصفية إلى ملفات .engine؛ أزلها حتى يتمكن Triton من تحميل خطة TensorRT الخام
with open(engine_path, "rb") as f:
meta_len = int.from_bytes(f.read(4), byteorder="little") # طول ترويسة البيانات الوصفية بصيغة JSON
f.seek(4 + meta_len)
plan = f.read()
Path("model_repository/yolo_trt/1").mkdir(parents=True, exist_ok=True)
Path("model_repository/yolo_trt/1/model.plan").write_bytes(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 for the nms=False export)
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: لا يوجد بديل يعمل على CPU فقط
- مسار ثابت: تُحدَّد بنية المسار عند إنشائه، ولا يمكن تغييرها ديناميكيًا
fn.padيضيف الحشو إلى اليمين والأسفل فقط: استخدمfn.cropمعout_of_bounds_policy="pad"لتوسيط الحشو- لا يتوفر وضع rect: تنتج مسارات DALI مخرجات ذات حجم ثابت (مثل 640×640). ولا يُدعم وضع rect في
auto=Trueالذي ينتج مخرجات متغيرة الحجم (مثل 384×640). لاحظ أنه رغم دعم TensorRT لأشكال الإدخال الديناميكية، فإن مسار DALI ثابت الحجم يتوافق طبيعيًا مع محرك ثابت الحجم لتحقيق أعلى معدل إنتاجية - استهلاك الذاكرة مع مثيلات متعددة: قد يؤدي استخدام
instance_groupمعcount> 1 في Triton إلى استهلاك مرتفع للذاكرة. استخدم مجموعة المثيلات الافتراضية لنموذج DALI
الأسئلة الشائعة#
تعتمد الفائدة على مسار العمل لديك. عندما يكون الاستدلال على GPU سريعًا بالفعل باستخدام TensorRT، قد تصبح المعالجة المسبقة على CPU، التي تستغرق من 2 إلى 10 مللي ثانية، العامل الأكبر في التكلفة. يزيل DALI هذا الاختناق بتشغيل المعالجة المسبقة على GPU. وتظهر أكبر المكاسب مع المدخلات عالية الدقة (1080p و4K)، وأحجام الدفعات الكبيرة، والأنظمة ذات العدد المحدود من أنوية CPU لكل GPU.
نعم. استخدم
DALIGenericIteratorللحصول على مخرجاتtorch.Tensorالمعالجة مسبقًا، ثم مررها إلىmodel.predict(). ومع ذلك، تكون فائدة الأداء أكبر مع نماذج TensorRT، حيث يكون الاستدلال سريعًا جدًا بالفعل وتصبح المعالجة المسبقة على CPU عنق الزجاجة.يضيف
fn.padالحشو إلى الحافتين اليمنى والسفلية فقط. أماfn.cropمعout_of_bounds_policy="pad"فيوسّطان الصورة ويضيفان الحشو بالتساوي إلى جميع الجوانب، بما يطابق سلوكLetterBox(center=True)في Ultralytics.متطابقة تقريبًا. اضبط
antialias=Falseفيfn.resizeلمطابقةcv2.INTER_LINEARفي OpenCV. قد تحدث فروق طفيفة في الفاصلة العائمة (< 0.001) بسبب اختلاف الحساب على GPU عن CPU، لكن ليس لها تأثير قابل للقياس في الدقة في الكشف.CV-CUDA مكتبة أخرى من NVIDIA لمعالجة الرؤية باستخدام تسريع GPU. وتتيح التحكم في كل عامل تشغيل على حدة (مثل OpenCV، لكن على GPU) بدلًا من نهج المسارات الذي يتبعه DALI. يدعم
cvcuda.copymakeborder()في CV-CUDA تحديد الحشو لكل جانب صراحةً، ما يجعل letterbox المتمركز سهل التنفيذ. اختر DALI لسير العمل القائم على المسارات (خصوصًا مع Triton)، واختر CV-CUDA للتحكم الدقيق على مستوى عوامل التشغيل في شيفرة الاستدلال المخصصة.