الوحدة 5 — تحسين الرسم في ONNX Runtime
الرسم الذي يخرج من torch.onnx.export ليس بالضرورة الرسم الأكثر فاعليّة للتنفيذ. سلاسل من العمليّات التافهة تُنشأ لأسباب هيكليّة، ثوابت تُعاد حسابها، طبقات يمكن دمج ثلاث منها في واحدة. مهمّة مُحسِّن الرسم في ONNX Runtime هي إعادة كتابة الرسم إلى نسخة مكافئة عدديًّا لكن أسرع في التنفيذ. هذه الوحدة تُظهر الميكانيك وتُدرّبك على قياس المكسب.
دمج العمليّات: أشهر التحسينات
خذ سلسلة كلاسيكيّة في شبكة تلافيفيّة:
Conv → BatchNorm → ReLU
هذه ثلاث عُقد ONNX، ثلاث قراءات وثلاث كتابات في الذاكرة. لكنّ BatchNorm في الاستدلال هو مجرّد ضرب وجمع بثوابت (المتوسّط والانحراف المُتعلَّمَين). يمكن إذًا دمجه في أوزان Conv نفسها، فتصير سلسلة:
Conv (بأوزان جديدة) → ReLU
هذا يُسمّى دمج BN في Conv. المكسب: عمليّة أقلّ، قراءة/كتابة أقلّ في الذاكرة، سرعة أعلى ملحوظة على ResNet. هذا التحسين تلقائيّ عندما يُفعَّل التحسين المتقدّم.
أنماط أخرى مشابهة: Conv + Add + ReLU، Gemm + Add، MatMul + Add + Softmax في المحوّلات. ONNX Runtime يعرف عشرات الأنماط ويُطبِّقها كلّها.
طيّ الثوابت: حساب مسبق لكلّ ما يمكن
كثير من الرسوم تحتوي على فروع لا تعتمد إلّا على ثوابت. مثال شائع: Reshape(x, target_shape) حيث target_shape نفسه ينتج من Concat(Shape(y), Constant(5)). إذا كان y ثابتًا (وزنًا مثلًا)، فـShape(y) معلوم عند التصدير، وConcat يُنتج ثابتًا آخر. طيّ الثوابت يحسب هذا مرّة واحدة ويستبدل الفرع بأكمله بالنتيجة.
على شبكات NLP خصوصًا، حيث كثير من العمليّات على الأشكال تُنتج من قِيم ثابتة، هذا التحسين يمكن أن يُقلّص عدد العُقد إلى النصف.
مستويات التحسين الأربعة
يُصنِّف ONNX Runtime التحسينات في أربعة مستويات، يُفعَّل كلّ منها بشكل مستقلّ:
| المستوى | الاسم | ما يفعله |
|---|---|---|
| 0 | ORT_DISABLE_ALL | لا تحسين إطلاقًا |
| 1 | ORT_ENABLE_BASIC | طيّ الثوابت، إزالة العُقد الميتة، تبسيطات جبريّة |
| 2 | ORT_ENABLE_EXTENDED | دمج العمليّات (Conv+BN، MatMul+Add، إلخ) |
| 3 | ORT_ENABLE_ALL | تحسينات إضافيّة قد تكون خاصّة بمزوّد التنفيذ |
الافتراضيّ هو ORT_ENABLE_ALL. يُفعَّل عبر خيارات الجلسة:
import onnxruntime as ort
opts = ort.SessionOptions()
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
session = ort.InferenceSession(
"resnet18.onnx", sess_options=opts,
providers=["CPUExecutionProvider"],
)
في 99% من الحالات، أبقِ الافتراضيّ. المستوى 0 مفيد فقط للتشخيص: إذا شككت أنّ تحسينًا يُغيّر النتيجة (وهذا نادر جدًّا)، أَشغِلْ في المستوى 0 وقارن. سنعود إلى هذا في نهاية الوحدة.
حفظ الرسم المُحسَّن
يمكن أن تطلب من ONNX Runtime حفظ الرسم بعد التحسين، مرّة واحدة، لتفادي إعادة العمل عند كلّ إنشاء جلسة:
opts = ort.SessionOptions()
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
opts.optimized_model_filepath = "resnet18-optimise.onnx"
_ = ort.InferenceSession("resnet18.onnx", sess_options=opts,
providers=["CPUExecutionProvider"])
# resnet18-optimise.onnx مكتوب على القرص
الملفّ الناتج نموذج ONNX عاديّ يمكن قراءته بـNetron ورؤية التحسينات مطبَّقة: أقلّ عُقد، أسماء طبقات مدمجة (Conv_BN_Relu مثل ًا). في الإنتاج، يوفّر تحميل هذا الملفّ ثانية أو أكثر من زمن الإقلاع، وهو مهمّ في السياقات ذات التوسّع التلقائيّ حيث تُنشأ حاويات جديدة كثيرًا.
تحذير مُهمّ: الرسم المُحسَّن قد يحتوي عمليّات خاصّة بمزوّد تنفيذ معيّن (FusedConv لـCPU مثلًا). لن يعمل على مزوّد مختلف. القاعدة: احفظ رسمًا مُحسَّنًا لكلّ مزوّد إذا كنت تنشر على أكثر من واحد، أو استعمل التحسين الديناميكيّ عند بدء الجلسة.
قياس المكسب: قبل وبعد
الجملة «التحسين يُسرِّع» بلا رقم لا تعني شيئًا. القياس الأدنى:
import time
import numpy as np
import onnxruntime as ort
def mesure(session, entree, nb=100, echauffement=20):
for _ in range(echauffement):
session.run(None, {"image": entree})
debut = time.perf_counter()
for _ in range(nb):
session.run(None, {"image": entree})
return (time.perf_counter() - debut) / nb * 1000 # ms
x = np.random.randn(1, 3, 224, 224).astype(np.float32)
sans = ort.SessionOptions()
sans.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
s0 = ort.InferenceSession("resnet18.onnx", sess_options=sans,
providers=["CPUExecutionProvider"])
avec = ort.SessionOptions()
avec.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
s3 = ort.InferenceSession("resnet18.onnx", sess_options=avec,
providers=["CPUExecutionProvider"])
print(f"بدون تحسين : {mesure(s0, x):.2f} ms/طلب")
print(f"مع تحسين : {mesure(s3, x):.2f} ms/طلب")
على ResNet18 قياسيّ بـCPU حديث، تتوقّع مكسبًا من 10% إلى 30%. على شبكات المحوّلات، المكسب أكبر عادةً (30% إلى 60%) لأنّها تحتوي على أنماط دمج أكثر.
المُحسِّن المستقلّ: onnxoptimizer
مكتبة onnxoptimizer (منفصلة عن ONNX Runtime) تسمح بتطبيق التحسينات وحفظ الرسم دون تشغيل جلسة. مفيدة عند تحضير نموذج لمنصّة لا تدعم مُحسِّنَ ONNX Runtime بكامله:
import onnx
import onnxoptimizer
modele = onnx.load("resnet18.onnx")
passes = ["fuse_bn_into_conv", "eliminate_identity", "fuse_add_bias_into_conv"]
optimise = onnxoptimizer.optimize(modele, passes)
onnx.save(optimise, "resnet18-post-optim.onnx")
الحذر: ليست كلّ الأدوات متساوية. مُحسِّن ONNX Runtime هو الأكثر تطوّرًا؛ onnxoptimizer أبسط لكنّه محدود. استعمله عند حاجة صريحة، وإلّا فالتحسين التلقائيّ في ONNX Runtime يكفي.
متى نُعطِّل التحسين؟
سيناريو نادر لكنّه مُلزم: إذا لاحظت اختلافًا عدديًّا كبيرًا بين النموذج المُدخَل وناتج ONNX Runtime، لكن ORT_DISABLE_ALL يُلغيه، فهناك تحسين يُدهور الدقّة. هذا نادر جدًّا لكنّه يحدث، خصوصًا مع الشبكات الحديثة التي تستخدم عمليّات غير قياسيّة. الإبلاغ عن ذلك مع الرسم الأدنى المُعيد لإنتاج المشكلة يُساعد فريق ONNX Runtime على إصلاح الحالة.
خارج ذلك، لا تُعطِّل التحسين أبدًا في الإنتاج. الفارق في الأداء لا يُبرَّر بأيّ ادّعاء «الوضوح».
قبل أيّ نشر إنتاجيّ، أنجز هذه القائمة. أوّلًا، حمِّل نموذج ONNX الأصليّ بـNetron وعُدَّ العُقد. ثانيًا، احفظ نسخة مُحسَّنة بـoptimized_model_filepath واحملها في Netron: يجب أن يكون العدد أقلّ بوضوح، وأسماء مثل FusedConv ظاهرة. ثالثًا، قِس زمن الاستدلال في الحالتين على 100 عيّنة. رابعًا، تأكّد أنّ التكافؤ العدديّ (وحدة 4) لا يزال مُرضِيًا مع النموذج المُحسَّن. إن نجحت الأربع، انشر النسخة المُحسَّنة.
الخلاصة
- تحسين الرسم يعيد كتابته إلى نسخة مكافئة عدديًّا وأسرع في التنفيذ.
- التحسينات الرئيسيّة: دمج العمليّات (Conv+BN+ReLU)، طيّ الثوابت، وإزالة العُقد الميتة.
- المستويات الأربعة تُتحكَّم بها عبر
graph_optimization_level؛ORT_ENABLE_ALLافتراضيّ ومناسب. - استعمل
optimized_model_filepathلتوليد ملفّ محسَّن مسبقًا، مع الحذر أنّه قد يكون خاصًّا بمزوّد تنفيذ.
الوحدة التالية: التكميم، الخطوة التي تُقلّص حجم النموذج وتُضاعف سرعته على العتاد المن اسب.