انتقل إلى المحتوى الرئيسي

الوحدة 7 — مزوّدو التنفيذ: CPU وGPU وTensorRT

رسم ONNX واحد يمكن أن يعمل على عشرات المنصّات: وحدة معالجة مركزيّة عاديّة، بطاقة NVIDIA بـTensorRT، شريحة Intel بـOpenVINO، هاتف بـCoreML، متصفّح بـWebAssembly. الأداة التي تُحقّق هذا هي مزوّد التنفيذ (Execution Provider، أو EP). كلّ EP يعرف كيف يُترجم عمليّات ONNX إلى تنفيذ على عتاده. هذه الوحدة تُظهر كيف نختار الترتيب الصحيح، ولماذا يهمّ.

قائمة المزوّدين الرئيسيّة

المزوّدالعتاد المستهدفالحزمة pip
CPUExecutionProviderكلّ CPU (افتراضيّ)onnxruntime
CUDAExecutionProviderNVIDIA GPU (CUDA)onnxruntime-gpu
TensorrtExecutionProviderNVIDIA GPU مع TensorRTonnxruntime-gpu + TensorRT
DmlExecutionProviderWindows DirectML (كلّ GPU)onnxruntime-directml
OpenVINOExecutionProviderIntel CPU/GPU/VPUonnxruntime-openvino
CoreMLExecutionProviderApple Silicononnxruntime-coreml

قائمة الاستيراد يمكن استعراضها في أيّ وقت:

import onnxruntime as ort
print(ort.get_available_providers())
# مثال : ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider']

آليّة الترتيب: يعمل EP الأوّل، وإلّا يمرّر إلى التالي

عند إنشاء الجلسة، تُقدّم قائمة مرتّبة:

session = ort.InferenceSession(
"resnet18.onnx",
providers=[
"TensorrtExecutionProvider",
"CUDAExecutionProvider",
"CPUExecutionProvider",
],
)

عند كلّ عقدة في الرسم، ONNX Runtime يسأل EP الأوّل: «هل تعرف كيف تُنفّذ هذه العمليّة؟». إذا نعم، يستعملها. إذا لا، يسأل الثاني، ثمّ الثالث. النتيجة: ترتيب الأولويّة يُقرّر أين تعمل كلّ عقدة. القاعدة العامّة: الأسرع أوّلًا، الأعمّ أخيرًا. الأخير يجب أن يكون دائمًا CPUExecutionProvider لأنّه المزوّد الوحيد الذي يدعم كلّ العمليّات المُعرَّفة في ONNX.

المزلّة الكبرى: الاعتماد الصامت على CPU

المشكلة تظهر هنا. تظنّ أنّ نموذجك يعمل على GPU لأنّك أدرجت CUDAExecutionProvider أوّلًا. لكن إذا كانت CUDA غير مُثبَّتة بشكل صحيح، أو الإصدار غير متوافق مع ONNX Runtime، يُسقطها EP بصمت ويستعمل CPU لكلّ الرسم. لا خطأ، لا تحذير، لا شيء. الاستدلال يعمل، لكن أبطأ عشر مرّات ممّا يجب.

الحلّ صريح ومُوصى به: تحقّق فورًا بعد إنشاء الجلسة أنّ المزوّدين المتوقّعين مفعَّلون:

session = ort.InferenceSession(
"resnet18.onnx",
providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
)
utilises = session.get_providers()
print("مزوّدو التنفيذ المستعملون :", utilises)

if "CUDAExecutionProvider" not in utilises:
raise RuntimeError("CUDA لم يُفعَّل: افحص إصدار onnxruntime-gpu وCUDA")

هذا الفحص يجب أن يكون في كلّ سكربت إنتاجيّ. تكلفته صفر، ويُنقذ من أسابيع تشخيص بلا سبب.

خيارات الجلسة العامّة

SessionOptions تحمل خيارات لا علاقة لها بمزوّد بعينه:

opts = ort.SessionOptions()
opts.intra_op_num_threads = 4 # خيوط داخل عمليّة واحدة
opts.inter_op_num_threads = 2 # خيوط لعمليّات متوازية
opts.log_severity_level = 2 # 0=مطوّل، 4=صامت
opts.enable_profiling = True # حفظ ملفّ تنميط لكلّ استدلال

session = ort.InferenceSession(
"resnet18.onnx", sess_options=opts,
providers=["CPUExecutionProvider"],
)

intra_op_num_threads = 0 يعني «كلّ الأنوية»؛ في محاويات مع حصص CPU مُحدَّدة، اضبط الرقم يدويًّا لتفادي المنافسة. enable_profiling يُنتج ملفّ JSON قابلًا للفتح في Chrome (chrome://tracing)، مفيد لتحديد العمليّات الأبطأ.

خيارات CUDA

CUDAExecutionProvider يقبل خيارات في شكل قاموس:

providers = [
("CUDAExecutionProvider", {
"device_id": 0,
"arena_extend_strategy": "kNextPowerOfTwo",
"gpu_mem_limit": 2 * 1024**3, # 2 غيغابايت
"cudnn_conv_algo_search": "EXHAUSTIVE",
}),
"CPUExecutionProvider",
]
session = ort.InferenceSession("resnet18.onnx", providers=providers)

device_id يختار GPU (في خادم بعدّة بطاقات). gpu_mem_limit سقف لذاكرة GPU، مفيد للتشارك بين خدمات. cudnn_conv_algo_search="EXHAUSTIVE" يستكشف كلّ خوارزميّات cuDNN عند التسخين لاختيار الأسرع؛ يُطيل الإقلاع بضع ثوان لكنّه يُسرِّع الاستدلالات اللاحقة.

TensorRT: أعلى سرعة، أطول إقلاع

TensorRT هو محرّك NVIDIA المتخصّص. عند تنشيطه، ONNX Runtime يمرّر الرسم (أو أجزاءً منه) إلى TensorRT الذي يبني محرّك تنفيذ خاصّ بالنموذج والبطاقة. البناء يستغرق دقائق (يستكشف TensorRT خيارات دمج وأحجام حزم متعدّدة)، لكنّ الاستدلال بعده أسرع، غالبًا مرّتين إلى أربع مرّات من CUDA وحده.

المشكلة: هذا البناء يتكرّر عند كلّ إقلاع للجلسة. الحلّ: ذاكرة الكاش للمحرّك:

providers = [
("TensorrtExecutionProvider", {
"trt_engine_cache_enable": True,
"trt_engine_cache_path": "./trt_cache",
"trt_fp16_enable": True, # فاعليّة إضافيّة بنصف الدقّة
"trt_max_workspace_size": 2 * 1024**3,
}),
"CUDAExecutionProvider",
"CPUExecutionProvider",
]

عند الإقلاع الأوّل، TensorRT يبني ويحفظ في ./trt_cache. الإقلاعات اللاحقة تُعيد التحميل من القرص في ثوانٍ. الكاش خاصّ ببطاقة GPU وإصدار TensorRT وإصدار CUDA: تغيير أيّ منها يستوجب إعادة البناء. في CI/CD، احفظ الكاش كأداة تحفيظ لتفادي 5 دقائق إضافيّة في كلّ نشر.

trt_fp16_enable=True يفعّل نصف الدقّة تلقائيًّا حيث يستطيع، مع مكسب سرعة إضافيّ. جرِّبه واقس الدقّة بعده (وحدة 4).

نموذج الترتيب النموذجيّ للإنتاج

استراتيجيّة موصى بها لخادم إنتاج على GPU:

providers = [
("TensorrtExecutionProvider", {
"trt_engine_cache_enable": True,
"trt_engine_cache_path": "/var/cache/trt",
"trt_fp16_enable": True,
}),
"CUDAExecutionProvider",
"CPUExecutionProvider",
]

session = ort.InferenceSession(MODELE, providers=providers)

# تحقّق شامل
utilises = session.get_providers()
if "TensorrtExecutionProvider" not in utilises:
logging.warning("TensorRT غير متاح: نستعمل CUDA")
if "CUDAExecutionProvider" not in utilises:
raise RuntimeError("GPU غير متاح، ترفض الخدمة الإقلاع")

هذه الاستراتيجيّة تعطي أفضل أداء ممكن، مع سلوك تراجع مُتحكَّم فيه: إذا سقط TensorRT، CUDA يعوّض؛ لكن إذا سقط CUDA، الخدمة ترفض الإقلاع بدل الوقوع في CPU البطيء.

بعض العُقد على GPU، بعضها على CPU

عندما يحتوي الرسم على عمليّة لا يدعمها EP GPU، ONNX Runtime يقسّم تلقائيًّا: العمليّات المدعومة تعمل على GPU، والباقي على CPU. كلّ انتقال يستوجب نقل التنسور بين ذاكرة CPU وGPU، وهذا مكلف. راقب هذا بـsession.get_provider_options() وبـenable_profiling. إذا كانت الجزئيّات كثيرة، إمّا ترقية opset (يضيف دعمًا)، وإمّا إعادة كتابة الجزء المسبِّب (وحدة 9).

الخلاصة

  • مزوّد التنفيذ يُترجم رسم ONNX إلى تنفيذ على العتاد؛ الترتيب يُحدّد الأولويّة.
  • ضع الأسرع أوّلًا (TensorRT/CUDA)، وأبقِ CPU الأخير كضامن.
  • تحقّق دائمًا من session.get_providers() لتفادي الاعتماد الصامت على CPU.
  • استعمل كاش المحرّك لـTensorRT لتفادي دقائق البناء عند كلّ إقلاع.

الوحدة التالية: كيف نقيس أداء نموذج ONNX بشكل قابل للتكرار، ونقارن بالطرق المرجعيّة.