الوحدة 10 — تقديم نموذج ONNX للخدمة خلف FastAPI
كلّ ما بنيناه من الوحدة 1 إلى 9 يخدم غرضًا وحيدًا: أن يصير النموذج متاحًا لاستدعاء من عميل حقيقيّ. رسم ONNX مُحسَّن ومكمَّم ومقيَّد الدقّة، بلا واجهة استدعاء، هو ملفّ على القرص، لا نظام إنتاج. هذه الوحدة تُغلق الحلقة: FastAPI أمام ONNX Runtime، بأنماط تصمد في الطلبات الحقيقيّة، مع نظرة على تقديم النموذج نفسه في المتصفّح.
المعادلة: جلسة واحدة، عدّة طلبات
الخطأ الأشيع للمبتدئ هو إنشاء InferenceSession جديدة في كلّ طلب. تكلفة الإنشاء ثقيلة: تحميل الملفّ، بناء الرسم، اختيار الخوارزميّات، حجز الذاكرة. القياس النموذجيّ على ResNet18: 200 مِلّي ثانية لإنشاء الجلسة مقابل 4 مِلّي ثانية للاستدلال الفعليّ. ع مليًّا، الإنشاء المتكرّر يقتل الأداء ويجعل الخدمة عاجزة عن تحمّل حمل بسيط.
القاعدة الذهبيّة: أنشئ الجلسة مرّة واحدة عند إقلاع الخدمة، شاركها بين كلّ الطلبات. ONNX Runtime مصمَّم لهذا: session.run() آمن للاستخدام المتزامن من عدّة خيوط، لأنّ الأوزان للقراءة فقط، والحالة المؤقّتة تُخصَّص لكلّ استدعاء.
import onnxruntime as ort
# مرّة واحدة عند إقلاع التطبيق
_session_globale = None
def initialiser():
global _session_globale
opts = ort.SessionOptions()
opts.intra_op_num_threads = 4
opts.inter_op_num_threads = 1
_session_globale = ort.InferenceSession(
"resnet18.onnx",
sess_options=opts,
providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
)
# فحص مزوّد التنفيذ (وحدة 7)
print("مزوّدون :", _session_globale.get_providers())
def predire(image_np):
return _session_globale.run(None, {"image": image_np})[0]
intra_op_num_threads يضبط الخيوط داخل عمليّة واحدة، وinter_op_num_threads بين عمليّات متوازية. في خدمة بـ8 خيوط CPU، الاختيار الشائع: intra=4 وinter=1 لطلب واحد كبير، أو intra=1 وinter=1 لعدّة طلبات متوازية يديرها إطار الخدمة نفسه (uvicorn workers مثلًا). القاعدة: لا تجعل الرقم الإجماليّ يتجاوز عدد الأنوية الحقيقيّة، وإلّا تتنافس الخيوط.