الوحدة 9 — قياس زمن الاستجابة واستهلاك الطاقة
الميزانيّة أُعلنَت في الوحدة 1، والاستراتيجيّات طُبِّقت من الوحدة 2 إلى 8. الآن يجب أن نتحقّق بالأرقام أنّ الوعود قد احتُرمَت. القياس على الجهاز ليس رفاهيّة، بل الاختبار الوحيد المقبول: خسارة نقطة دقّة تُقاس على مجموعة تحقّق، لكنّ زمن الاستجابة الفعليّ في يد المستخدم يعتمد على العتاد، ودرجة الحرارة، وتصميم الخيوط، ونسخة نظام التشغيل. سنشرح في هذه الوحدة كيف نجري قياسات نظيفة تُقنع فريق المنتج، وكيف نُقدِّر استهلاك الطاقة بطريقة يُمكن تكرارها.
أدوات القياس الرسميّة
TFLite يقدّم أداتَين أساسيّتَين، مكتوبتَين بـC++ ومُقدَّمتَين ثنائيًّا:
Benchmark Tool للأندرويد: ملفّ ثنائيّ يُثبَّت على الهاتف عبر adb، يُشغِّل النموذج مئات ال مرّات، ويعطي إحصائيّات مفصّلة عن كلّ عمليّة داخل الرسم. يُظهر أيضًا كم عمليّة رُحِّلت إلى المفوَّض وكم بقيت على CPU.
iOS Benchmark: ملفّ Xcode Playground يُنفَّذ داخل محاكي Xcode أو على جهاز حقيقيّ. أقلّ تفصيلًا من نظيره Android، لكنّه يعطي الأرقام الأساسيّة.
قياس زمن الاستدلال الأدنى في بايثون
قبل نقل النموذج إلى الهاتف، نُجري قياسًا مرجعيًّا على المكتب لتقدير الاتّجاه العامّ:
import time
import numpy as np
import tensorflow as tf
interpreter = tf.lite.Interpreter(model_path="plantvillage_int8.tflite")
interpreter.allocate_tensors()
inp = interpreter.get_input_details()[0]
out = interpreter.get_output_details()[0]
# صورة اختبار عشوائيّة بحجم الإدخال
image = np.random.randint(-128, 127, inp["shape"], dtype=np.int8)
# 1) الإحماء : تحميل الأوزان في الذاكرة السريعة، تحسينات JIT
for _ in range(10):
interpreter.set_tensor(inp["index"], image)
interpreter.invoke()
# 2) القياس الفعليّ
latences_ms = []
for _ in range(200):
t0 = time.perf_counter()
interpreter.set_tensor(inp["index"], image)
interpreter.invoke()
_ = interpreter.get_tensor(out["index"])
latences_ms.append((time.perf_counter() - t0) * 1000)
latences_ms = np.array(latences_ms)
print(f"وسيط : {np.median(latences_ms):.2f} ms")
print(f"المئينيّ 90 : {np.percentile(latences_ms, 90):.2f} ms")
print(f"المئينيّ 99 : {np.percentile(latences_ms, 99):.2f} ms")
print(f"أقصى : {latences_ms.max():.2f} ms")
لماذا الإحماء (warm-up) ضروريّ
القياس الأوّل بعد allocate_tensors يستغرق أحيانًا أضعاف الوسيط. الأسباب:
- الأوزان لم تُحمَّل في ذاكرة L2: أوّل استدلال يجلبها من ذاكرة الجهاز الأبطأ.
- المفوَّض يُهيّئ نفسه: NNAPI مثلًا يجمع الرسم ويُحسِّنه، وهذه العمليّة مرّة واحدة تُكلِّف مِلّي ثانية إلى ثانية.
- CPU يرفع تردّده: نُوى الهاتف تعمل عند تردّد منخفض في وضع الخمول، وتحتاج بضع مِلّي ثوانٍ لتصل إلى أقصى تردّد بعد بدء الحمل.
نتجاهل أوّل 10 إلى 20 استدلالًا في القياس. عدد الاستدلالات الفعليّة يجب أن يكون 200 على الأقلّ ليكون الإحصاء ذا معنى.
لماذا الوسائط المئويّة لا المتوسّط
المتوسّط يُخفي القمم. مستخدم يرى وسيطًا 30 مِلّي ثانية سيكون سعيدًا، لكن إن كان المئينيّ 99 عند 500 مِلّي ثانية، فسيرى تجميدًا مُزعجًا مرّة كلّ مئة استدلال. المتوسّط قد يكون 40 مِلّي ثانية بلا أن يُبيِّن هذه القمّة.
القاعدة العمليّة: نقيس دومًا الوسيط والمئينيّ 90 والمئينيّ 99. الوسيط يُقاس فيه أداء التطبيق العاديّ، والمئينيّ 99 يُقاس فيه أسوأ الحالات التي سيراها المستخدم.
القياس على Android بـBenchmark Tool
تحميل الأداة وتشغيلها:
# 1) تحميل الملفّ الثنائيّ (يُنشَر مع TFLite على GitHub)
wget https://storage.googleapis.com/tflite-benchmarks/android_arm64_benchmark_model
# 2) نقل إلى الهاتف عبر adb
adb push android_arm64_benchmark_model /data/local/tmp/benchmark
adb push plantvillage_int8.tflite /data/local/tmp/
adb shell chmod +x /data/local/tmp/benchmark
# 3) تشغيل مع NNAPI
adb shell /data/local/tmp/benchmark \
--graph=/data/local/tmp/plantvillage_int8.tflite \
--num_threads=4 \
--num_runs=200 \
--warmup_runs=20 \
--use_nnapi=true \
--report_delegated_partition_size=true
الخرج المتوقَّع:
Initialized session in 42.318ms
Running benchmark for 20 iterations
Running benchmark for 200 iterations
Inference timings in us: Init: 42318, First inference: 34521,
Warmup (avg): 19842, Inference (avg): 18234
Delegated partition sizes: 89 / 92 operators on NNAPI
القراءة: 89 عمليّة من أصل 92 نُفِّذت على NNAPI (رائع)، ومتوسّط الاستدلال 18.2 مِلّي ثانية.
قياس استهلاك الطاقة على Android
الطاقة أصعب من الزمن لأنّها تحتاج قياسًا مادّيًّا. حلّان عمليّان:
الحلّ الأوّل: Battery Historian (رسميّ من Google). يجمع سجلّات dumpsys batterystats عبر ساعة استعمال، ثمّ يُحوِّلها إلى مخطّطات:
# 1) تصفير العدّاد وتشغيل التطبيق لساعة
adb shell dumpsys batterystats --reset
# ... استعمال التطبيق ...
adb bugreport > bugreport.zip
# 2) تحميل Battery Historian وتشغيله
git clone https://github.com/google/battery-historian
docker run -p 9999:9999 battery-historian
# افتح http://localhost:9999 وارفع bugreport.zip
الحلّ الثاني: قياس متزامن بـmonsoon أو Yokogawa. مصدر تغذية مُتحكَّم فيه يُقاس تيّاره باستمرار، وهو أدقّ لكنّه يكلّف بضع مئات من الدولارات ($300 تقريبًا للـmonsoon HV).
للتقدير السريع، نستعمل الصيغة العمليّة:
طاقة الاستدلال ≈ متوسّط تيّار الهاتف تحت الحمل × الفولطيّة × زمن الاستدلال
على هاتف Snapdragon 680: التيّار تحت الحمل نحو 400 mA، الفولطيّة 3.8 V، فطاقة الاستدلال 20 مِلّي ثانية تكون 400 × 3.8 × 0.020 = 30.4 مِلّي جول. الميزانيّة كانت 40 مِلّي جول، فنحن ضمنها.
قياس استهلاك الطاقة على iOS
Xcode Instruments يقدّم Energy Log الذي يُقاس فيه استهلاك الطاقة الكلّيّ للتطبيق بمقياس نسبيّ (Low → High). المقياس أقلّ دقّة من Android، لكنّه يُظهر الاتّجاه: إن قفز المؤشِّر إلى High أثناء الاستدلال، ثمّة مشكلة (استعمال GPU حين لا يلزم مثلًا).
Xcode 15 وما بعده يقدِّم أيضًا Metal System Trace الذي يُظهر ما إن كان GPU يشتغل، وهو مفيد للتحقّق من أنّ Neural Engine لا GPU هو الذي ينفِّذ الاستدلال.