المعالجة المسبقة المسرَّعة بواسطة GPU باستخدام NVIDIA DALI#
عند نشر نماذج Ultralytics YOLO في بيئة الإنتاج، غالبًا ما تصبح المعالجة المسبقة عنق الزجاجة. وبينما يستطيع TensorRT تنفيذ استدلال النموذج خلال بضعة أجزاء من الثانية فقط، قد تستغرق المعالجة المسبقة المعتمدة على CPU (تغيير الحجم، وإضافة الحواف، والتطبيع) مدةً تتراوح بين 2 و10 مللي ثانية لكل صورة، لا سيما عند الدقات العالية. تحل NVIDIA DALI (مكتبة تحميل البيانات (DALI)) هذه المشكلة بنقل مسار المعالجة المسبقة بأكمله إلى GPU.
يشرح لك هذا الدليل كيفية إنشاء مسارات DALI تكرر المعالجة المسبقة لـ 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), # 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 القياسي.
المسار المتمركز (موصى به، ويطابق 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.
"""
# 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 بدقة.
يضيف معامل DALI fn.pad حواف إلى الجانبين الأيمن والسفلي فقط. وللحصول على حواف متمركزة (مطابقة لـ LetterBox(center=True) في Ultralytics)، استخدم fn.crop مع out_of_bounds_policy="pad". باستخدام crop_pos_x=0.5 وcrop_pos_y=0.5 الافتراضيين، تُوسَّط الصورة تلقائيًا مع حواف متناظرة.
يفعّل معامل DALI fn.resize منع التعرج افتراضيًا (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
# 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))ينطبق ذلك على جميع مسارات المعالجة المسبقة الخارجية — إدخال الموتر مباشرةً، وتدفقات الفيديو، والنشر باستخدام 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 عبر استخدام نموذج تجميعي. يؤدي ذلك إلى إلغاء المعالجة المسبقة على 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."""
# 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, dynamic=True, nms=False
) # NMS-free (N, 300, 6); TensorRT >= 8.5
# 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"
}
}
]
}يربط النموذج التجميعي النماذج عبر أسماء موترات افتراضية. تطابق قيمة "preprocessed_image" في output_map ضمن خطوة DALI قيمة "preprocessed_image" في input_map ضمن خطوة 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 GPU: لا يتوفر بديل يعمل على 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-10ms، التكلفة المهيمنة. يزيل DALI عنق الزجاجة هذا من خلال تشغيل المعالجة المسبقة على GPU. وتظهر أكبر المكاسب مع المدخلات عالية الدقة (1080p و4K)، وأحجام الدفعات الكبيرة، والأنظمة التي تحتوي على عدد محدود من أنوية CPU لكل GPU.
نعم. استخدم
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) بسبب اختلاف العمليات الحسابية بين GPU وCPU، لكنها لا تؤثر تأثيرًا قابلًا للقياس في دقة الكشف.تُعد CV-CUDA مكتبة أخرى من NVIDIA لمعالجة الرؤية المتسارعة باستخدام GPU. وهي توفر تحكمًا على مستوى كل عامل (مثل OpenCV، ولكن على GPU) بدلًا من نهج خط الأنابيب الذي يتبعه DALI. ويدعم
cvcuda.copymakeborder()في CV-CUDA تحديد الحشو صراحةً لكل جانب، ما يجعل letterbox المتمركز أمرًا مباشرًا. اختر DALI لسير العمل القائم على خطوط الأنابيب (خصوصًا مع Triton)، واختر CV-CUDA للتحكم الدقيق على مستوى العوامل في تعليمات الاستدلال البرمجية المخصصة.