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

الوحدة 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 هو الذي ينفِّذ الاستدلال.

المرجعيّة على ثلاث فئات من الهواتف

نُشغِّل النموذج نفسه (plantvillage_int8.tflite، 3.5 مِيغا) مع نفس الإعدادات على ثلاثة هواتف مختلفة، لنقيس اتّساق الأداء:

الهاتفالفئةالمفوَّضوسيطالمئينيّ 99طاقة/استدلال
Xiaomi Redmi 10C (Snapdragon 680)اقتصاديّةNNAPI18 ms42 ms30 mJ
Samsung Galaxy A54 (Exynos 1380)متوسّطةNNAPI12 ms28 ms24 mJ
iPhone 13 (A15 Bionic)راقيةCore ML6 ms14 ms12 mJ

الملاحظات:

  • الفارق الفئويّ ~3 أضعاف: هاتف راقٍ يستدلّ ثلاث مرّات أسرع، ويستهلك ثلث الطاقة تقريبًا.
  • المئينيّ 99 دومًا ضعف الوسيط تقريبًا: قمم مؤقّتة عاديّة، ولا يجب أن تُفزع.
  • الجميع ضمن ميزانيّة 60 مِلّي ثانية: حتّى الهاتف الاقتصاديّ يحترم الوعد.
اختلاف بين هاتف الفريق وهاتف المستخدم

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

قياس الاتّساق: التغيّر مع درجة الحرارة

عند تشغيل التطبيق دقيقتَين متواصلتَين، الهاتف يسخن، والنظام يخفض تردّد CPU لحماية العتاد (thermal throttling). زمن الاستجابة يقفز 30 % إلى 80 %. لتقييم هذا:

# قياس الوسيط كلّ 30 ثانية على مدى 10 دقائق
adb shell /data/local/tmp/benchmark \
--graph=/data/local/tmp/plantvillage_int8.tflite \
--num_runs=1000000 \
--run_delay=0.01 \
--enable_op_profiling=true \
--output_prefix=thermal

على هاتفنا المرجعيّ، الوسيط ينتقل من 18 مِلّي ثانية عند البدء إلى 28 مِلّي ثانية بعد ثماني دقائق. هذا يفرض قرارًا معماريًّا: هل نتحمّل الارتفاع، أم نُضيف تباعدًا بين الاستدلالات (30 مِلّي ثانية بدل صفر) لإتاحة تبريد الهاتف؟

الخلاصة

  • قِس دومًا الوسيط والمئينيّ 90 والمئينيّ 99: المتوسّط يُخفي القمم؛ المستخدم يشعر بها. 200 استدلال بعد إحماء 20 هو الحدّ الأدنى للإحصاء ذي معنى.
  • الإحماء ليس اختياريًّا: أوّل استدلال قد يكون عشرة أضعاف الوسيط بسبب تحميل الأوزان وتهيئة المفوَّض ورفع تردّد CPU. تجاهله دومًا.
  • الطاقة تُقاس بأدوات مخصَّصة: Battery Historian لتقدير على مستوى الجلسة، وmonsoon لدقّة عالية على مستوى الاستدلال. الصيغة العمليّة (تيّار × فولطيّة × زمن) كافية لأوّل تقدير.
  • اختبر على ثلاث فئات من الهواتف: هاتف الفريق ليس هاتف المستخدم. الفارق يصل إلى ثلاثة أضعاف، وأيّ قرار مبنيّ على هاتف واحد يخون الفئة الاقتصاديّة.

الوحدة التالية تجمع كلّ ما بنيناه في مشروع كامل: تطبيق موبايل يستعمل النموذج على أرض الواقع، مع تحديث النموذج عن بُعد، وجدول نهائيّ يوثّق كلّ ما تنازلنا عنه في الطريق.