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

الوحدة 8 — القياس المقارن لأداء نماذج ONNX

«ONNX أسرع» — عبارة صحيحة كثيرًا لكنّها بلا معنى بلا رقم ولا ظرف. القياس السليم يحتاج بروتوكولًا يمكن تكراره، وأدوات صحيحة، ووعيًا بالمزالق التي تُنتج أرقامًا مضلِّلة. هذه الوحدة تُقدّم ما يكفي لإنشاء تقارير أداء يمكن الدفاع عنها.

القاعدة الأولى: التسخين

عند أوّل استدلال، تحدث أشياء كثيرة مرّة واحدة فقط: تخصيص الذاكرة، تحميل الأوزان إلى ذاكرة GPU، تجميع نوى CUDA/cuDNN، بناء محرّك TensorRT، تفعيل خيوط التنفيذ. إذا قست الاستدلال الأوّل، ستحصل على رقم يمكن أن يكون مئة مرّة أكبر من الاستدلال العاديّ.

القاعدة الصارمة: 20 إلى 50 استدلال تسخين قبل أيّ قياس:

import time
import numpy as np

def mesure(session, entree, nb=200, echauffement=50):
# التسخين: لا نُخزّن الأزمنة
for _ in range(echauffement):
session.run(None, {"image": entree})

# القياس الحقيقيّ
zmns = []
for _ in range(nb):
d = time.perf_counter()
session.run(None, {"image": entree})
zmns.append((time.perf_counter() - d) * 1000) # ms
return np.array(zmns)

x = np.random.randn(1, 3, 224, 224).astype(np.float32)
z = mesure(session, x)
print(f"n={len(z)} | متوسط={z.mean():.2f} | p50={np.median(z):.2f} "
f"| p95={np.percentile(z, 95):.2f} | p99={np.percentile(z, 99):.2f} ms")

عرض النسب المئويّة، لا المتوسّط وحده

الاستدلال الفرديّ يتذبذب: عمليّات نظام في الخلفيّة، تحويل خيط CUDA، إعادة تخصيص ذاكرة صغرى. المتوسّط وحده يُخفي هذه القفزات؛ النسبة المئويّة 99 هي ما يواجهه المستخدم على الطلب الأسوأ من كلّ مئة.

في تقرير قياس جدّي، اعرض دائمًا: p50 (متوسط)، p95، p99، وأقصى. p99 = 3 × p50 يعني أنّ 1% من المستخدمين يواجهون تأخيرًا ثلاث مرّات أكبر. هذا واقعك، لا وهم المتوسّط.

حجم الحزمة: البُعد المنسيّ

قياس على حزمة من 1 لا يُخبر شيئًا عن أداء الحزمة من 32. الاثنان مختلفان جدًّا:

  • الحزمة 1: التأخير للمستخدم الفرديّ. الأمثل لتطبيقات تفاعليّة (روبوت محادثة، بحث بصريّ).
  • الحزمة 32-128: الإنتاجيّة الكلّيّة (طلبات/ثانية). الأمثل للمعالجة الجماعيّة (تصنيف صور مستودع كامل، فهرسة).

القياس الصحيح يعطي جدولًا لعدّة أحجام حزم:

import matplotlib.pyplot as plt

resultats = {}
for taille in [1, 4, 8, 16, 32, 64]:
x = np.random.randn(taille, 3, 224, 224).astype(np.float32)
z = mesure(session, x, nb=100, echauffement=20)
par_image = z.mean() / taille
resultats[taille] = par_image
print(f"حزمة {taille:3d} : {z.mean():.2f} ms/دفعة | {par_image:.2f} ms/صورة")

النتيجة النموذجيّة: زمن الاستدلال لكلّ صورة ينخفض مع كِبَر الحزمة (استغلال أفضل للمتاحيّة)، حتى نقطة تشبّع. معرفة هذه النقطة يُتيح ضبط حجم الحزمة في الإنتاج على النقطة الأمثل.

الأداة الرسميّة: onnxruntime_perf_test

ONNX Runtime يأتي بأداة سطر أوامر مخصّصة للقياس، أكثر دقّة من سكربتات بايثون اليدويّة:

onnxruntime_perf_test -e cuda -r 200 -c 4 -m times -M resnet18.onnx
  • -e cuda: مزوّد التنفيذ
  • -r 200: 200 استدلال
  • -c 4: تزامن (4 خيوط تُطلق طلبات موازية)
  • -m times: وضع القياس (بديل: duration)
  • -M: يستعمل ملفّ ONNX

المخرج يحتوي على متوسّط، انحراف معياريّ، p95، p99، عدد فشل. مفيد جدًّا لأنّه يقيس تكلفة الاستدعاء الحقيقيّة دون أعباء بايثون.

مقارنة PyTorch مقابل ONNX Runtime مقابل TensorRT

الجدول المرجعيّ الذي كلّ فريق إنتاج يحتاجه. مثال ملموس على ResNet18، حزمة 1:

# PyTorch, CPU
import torch
modele = models.resnet18().eval()
x_t = torch.randn(1, 3, 224, 224)
zt = mesure_torch(modele, x_t)

# ONNX Runtime, CPU
s_cpu = ort.InferenceSession("resnet18.onnx",
providers=["CPUExecutionProvider"])
zc = mesure(s_cpu, x_t.numpy())

# ONNX Runtime, CUDA
s_cuda = ort.InferenceSession("resnet18.onnx",
providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
zg = mesure(s_cuda, x_t.numpy())

# ONNX Runtime, TensorRT + FP16
providers = [("TensorrtExecutionProvider", {"trt_fp16_enable": True}),
"CUDAExecutionProvider", "CPUExecutionProvider"]
s_trt = ort.InferenceSession("resnet18.onnx", providers=providers)
zr = mesure(s_trt, x_t.numpy())

for etiquette, z in [("PyTorch CPU", zt), ("ONNX CPU", zc),
("ONNX CUDA", zg), ("ONNX TensorRT FP16", zr)]:
print(f"{etiquette:25s} : p50={np.median(z):6.2f} ms | p99={np.percentile(z,99):6.2f} ms")

نتيجة نموذجيّة على خادم بـRTX A4000:

البيئةp50 (ms)p99 (ms)التسريع
PyTorch CPU45.062.01.0×
ONNX Runtime CPU22.028.02.0×
ONNX Runtime CUDA3.55.812.9×
ONNX Runtime TensorRT FP161.42.132.1×

الأرقام تختلف حسب البطاقة والنموذج، لكنّ الترتيب النسبيّ ثابت في الغالب.

القياس على المُرمِّز النصّيّ

الخيط الأحمر الثاني يعطي دروسًا مختلفة. RNN صغيرة على تسلسلات قصيرة تُوفِّر أرباحًا مختلفة:

ids = np.random.randint(0, 30000, (1, 32)).astype(np.int64)

zt = mesure(s_torch_codeur, x=ids) # تنفيذ بايثون
zo = mesure(s_onnx_codeur, ids=ids) # ONNX Runtime

# النتيجة النموذجيّة:
# PyTorch : p50=1.8 ms
# ONNX RT : p50=0.6 ms (تسريع 3×)
# TensorRT : p50=0.5 ms (تسريع طفيف إضافيّ فقط)

على نماذج بحجم صغير مثل هذا، TensorRT لا يُضيف كثيرًا لأنّ التنفيذ يهيمن عليه استدعاء نواة CUDA وليس الحساب نفسه. القاعدة: النماذج الصغيرة تستفيد أقلّ من TensorRT؛ ركّز على تحسينات أخرى (تكميم، دمج طلبات).

المزالق الشائعة في القياس

قياس داخل جلسة PyTorch الأولى: torch.no_grad() منسيّ يُضاعف الزمن. تأكّد دائمًا:

modele.eval()
with torch.no_grad():
...

تجاهل نقل التنسور إلى/من GPU: في التطبيق الحقيقيّ، ينتقل التنسور من CPU إلى GPU قبل الاستدلال، ومن GPU إلى CPU بعده. إذا كان قياسك يفترض التنسور مسبق التحميل في GPU، فأنت تُقلّل من التكلفة الحقيقيّة. القِس نقلًا كاملًا حتى تكون النتيجة قابلة للاعتماد.

قياس على البطاقة الخاطئة: nvidia-smi تُظهر أنّ CUDA يستعمل GPU 1 بينما ONNX Runtime يستعمل GPU 0. أَشِر صراحةً بـCUDA_VISIBLE_DEVICES أو بخيار device_id.

قياس أثناء تدريب آخر: خادم GPU يشارك بين عمليّات؛ استدلالك يُبطأ لأنّ تدريبًا يُنافس على الذاكرة الحاسوبيّة. اقرأ nvidia-smi -l 1 أثناء القياس لتأكّد أنّك الوحيد.

البطاقات الشخصيّة ليست الخوادم

قياس على GPU سطح المكتب (RTX 4070، مثلًا) لا يتنبّأ دائمًا بأداء GPU الخادم (A100). البطاقات الاستهلاكيّة تفتقد إلى شرائح Tensor Cores الأحدث، وذاكرتها أضيق. تقاريرك يجب أن تكون على العتاد المستهدف للإنتاج. إذا لم يكن ذلك ممكنًا، وثِّق العتاد بوضوح في التقرير حتى يعرف القارئ حدود الأرقام.

الخلاصة

  • التسخين قبل القياس إلزاميّ: 20-50 استدلال لتفادي تكاليف الإقلاع.
  • اعرض النسب المئويّة (p50، p95، p99)، لا المتوسّط وحده.
  • قِس على عدّة أحجام حزم: الحزمة 1 للتفاعليّة، الحزمة الكبيرة للإنتاجيّة.
  • استعمل onnxruntime_perf_test كأداة مرجعيّة؛ قارن دائمًا PyTorch/ONNX/TensorRT في جدول واحد على العتاد الإنتاجيّ.

الوحدة التالية: ما نفعل عندما التصدير يفشل بسبب عمليّة غير مدعومة.