بسیاری از مدلهای بینایی ماشین در محیط آموزش عالی عمل میکنند، اما بهمحض انتقال به یک دستگاه امبدد، پهپاد یا دوربین مداربسته بهدلیل مصرف بالای رم و توان پردازشی متوقف میشوند. برای اجرای بلادرنگ، نمیتوان مدلهای حجیم را بدون تغییر روی سختافزارهای کممصرف مستقر کرد. در این درس یاد میگیرید که چطور محدودیت حافظه و پردازش را مدیریت کرده و با کاهش چند برابری حجم، سرعت استنتاج را بدون افت ملموس دقت افزایش دهید.
استفاده از سرورهای ابری خارجی برای استنتاج بلادرنگ علاوه بر تاخیر شبکه، برای کاربران داخل ایران بهدلیل چالشهای دسترسی به ارائهدهندگان ابری و هزینههای ارزی اجارهی GPU چندان بهصرفه یا در دسترس نیست؛ بنابراین اجرای محلی و استقرار روی سختافزارهای لبه یک مهارت حیاتی به شمار میآید. همچنین برای نصب بستههای بهینهسازی و کتابخانههای استنتاج از طریق PyPI ممکن است با نوسان شبکه روبهرو شوید که استفاده از مخازن آینهای داخلی این مشکل را رفع میکند.
در این بخش، سه ابزار اساسی یعنی انتخاب معماری متناسب (مانند MobileNetV3 یا ConvNeXt)، کوانتیزهسازی ایستای INT8 و استفاده از موتورهای اجرایی بهینهساز نظیر ONNX Runtime و TensorRT را بهصورت عملی پیادهسازی و ارزیابی میکنید.
استنتاج روی لبه (Edge inference) هنرِ اجرای مدلی با دقت 90% و نرخ 30 فریم بر ثانیه روی دستگاهی با 2 گیگابایت رم است؛ جایی که تکتک درصدهای دقت در ازای میلیثانیههای تاخیر معامله میشوند.
اهداف یادگیری
- اندازهگیری تاخیر استنتاج، اوج مصرف حافظه و نرخ پردازش (throughput) برای هر مدل PyTorch، و تحلیل مصالحه میان FLOPs، تعداد پارامترها و تاخیر
- کوانتیزهسازی یک مدل بینایی به INT8 با استفاده از کوانتیزهسازی پس از آموزش (PTQ) در PyTorch و تایید افت دقت کمتر از 1%
- خروجی گرفتن به فرمت ONNX و کامپایل با ONNX Runtime یا TensorRT؛ بههمراه نام بردن از سه خطای رایج خروجی گرفتن و روش رفع آنها
- توضیح زمان مناسب برای انتخاب MobileNetV3، EfficientNet-Lite، ConvNeXt-Tiny یا MobileViT با توجه به محدودیتهای لبه
صورت مسئله
مدل بینایی ماشین هنگام آموزش، یک غول ممیز شناور (floating-point) است: 100 میلیون پارامتر، 10 گیگافلاپس (GFLOPs) در هر گذر رو به جلو و 2 گیگابایت حافظهی گرافیکی. هیچکدام از اینها روی گوشی، سیستم چندرسانهای خودرو، دوربین صنعتی یا پهپاد جا نمیشوند. عملیاتیسازی یک سیستم بینایی یعنی گنجاندن همان پیشبینیها در بودجهای 100 برابر کوچکتر.
بخش عمدهی کار روی دوش سه اهرم اصلی است: انتخاب مدل (معماری کوچکتر با همان دستورالعمل آموزش)، کوانتیزهسازی (INT8 بهجای FP32) و محیط اجرای استنتاج (ONNX Runtime، TensorRT، Core ML، TFLite). تنظیم درست اینها مرز میان دمویی است که روی ورکاستیشن اجرا میشود با محصولی تجاری که روی یک ماژول دوربین 30 دلاری بالا میآید.
این درس ابتدا اصول سنجش دقیق را پایهگذاری میکند (چیزی را که نتوانید اندازه بگیرید، نمیتوانید بهینهسازی کنید)، سپس سراغ این سه اهرم میرود. هدف این نیست که تکتک محیطهای اجرای لبه را یاد بگیرید؛ بلکه باید بدانید چه ابزارهایی در دست دارید و چطور مطمئن شوید هر کدام دقیقاً همان کاری را میکنند که انتظار دارید.
مفاهیم کلیدی
سه بودجهی اصلی
- تاخیر (Latency): صدکهای p50، p95 و p99. گزارش صرفاً p50 (یا میانگین)، رفتار انتهای توزیع (tail behaviour) را پنهان میکند؛ در حالی که همین رفتار برای سیستمهای بیدرنگ (real-time) تعیینکننده است.
- اوج مصرف حافظه (Peak memory): بیشترین مقداری که دستگاه در تمام طول اجرا میبیند، نه میانگین حالت پایدار. از این جهت مهم است که خطای کمبود حافظه (OOM) روی سختافزارهای امبدد مرگبار است.
- توان / مصرف انرژی: میلیژول به ازای هر استنتاج روی دستگاههای مبتنی بر باتری. اغلب با حاصلضرب زمان در درصد استفاده از CPU/GPU تقریب زده میشود.
تصمیمگیری برای سامانههای لبه بر اساس جدولی از (مدل، تاخیر، حافظه، دقت) انجام میشود. تکتک خانههای این جدول باید روی دستگاه مقصد اندازه گرفته شوند، نه روی ورکاستیشن.
اصول سنجش دقیق
سه قاعدهای که در هر پروفایلگیری روی لبه باید رعایت شوند:
- گرم کردن (Warm up) مدل با 5 تا 10 گذر رو به جلوی آزمایشی پیش از اندازهگیری. کشهای سرد و کامپایل JIT اعدادی غیرواقعی در دورهای اول تولید میکنند.
- همگامسازی (Synchronise) پردازشهای GPU با
torch.cuda.synchronize()قبل و بعد از بلوک زمانگیری. بدون این کار، بهجای زمان اجرای واقعی کرنل، فقط زمان ارسال دستور به کرنل (dispatch) را اندازه میگیرید. - تثبیت ابعاد ورودی مطابق با رزولوشن محیط عملیاتی. تاخیر در ابعاد 224x224 با تاخیر در 512x512 برابر نیست.
FLOPs بهعنوان یک تخمینگر
تعداد کل محاسبات ممیز شناور (FLOPs) به ازای هر استنتاج، معیاری ارزان و مستقل از سختافزار برای تخمین تاخیر است؛ برای مقایسهی معماریها مفید است، اما بهعنوان زمان واقعی اجرا گمراهکننده خواهد بود. مدلی با 10% عملیات ممیز شناور بیشتر، در عمل ممکن است 2 برابر سریعتر باشد؛ چون از عملیاتهای سازگارتر با سختافزار استفاده میکند (کانولوشنهای depthwise عالی کامپایل میشوند، اما کانولوشنهای بزرگ 7x7 نه).
قاعده: از FLOPs برای جستوجوی معماری و از تاخیر واقعی روی دستگاه برای تصمیمگیری نهایی استقرار استفاده کنید.
کوانتیزهسازی در یک بند
وزنها و اکتیویشنهای FP32 را با INT8 جایگزین کنید. حجم مدل 4 برابر کاهش مییابد، پهنای باند حافظه 4 برابر آزاد میشود و محاسبات روی سختافزارهایی با کرنلهای INT8 (همهی تراشههای موبایل امروزی و همهی کارتهای گرافیک NVIDIA دارای هستهی تنسور (Tensor Core)) بین 2 تا 4 برابر سریعتر میشود. افت دقت در تسکهای بینایی با کوانتیزهسازی ایستای پس از آموزش معمولاً بین 0.1 تا 1 درصد است.
انواع:
- پویا (Dynamic) — وزنها به INT8 تبدیل میشوند، اما اکتیویشنها در قالب FP محاسبه میشوند. پیادهسازی آن ساده است و سرعت را کمی بالا میبرد.
- ایستا پس از آموزش (Static PTQ) — وزنها کوانتیزه میشوند و بازهی اکتیویشنها روی مجموعهی کوچکی از دادههای کالیبراسیون تنظیم میگردد. بسیار سریعتر از حالت پویا است.
- آموزش آگاه از کوانتیزهسازی (QAT) — اثر کوانتیزهسازی در حین آموزش شبیهسازی میشود تا مدل خود را با آن تطبیق دهد. بالاترین دقت را دارد، اما به دادههای برچسبدار نیاز دارد.
در بینایی ماشین، کوانتیزهسازی ایستای پس از آموزش 95% مزایا را با 5% زحمت فراهم میکند. فقط زمانی سراغ QAT بروید که افت دقت PTQ غیرقابلقبول باشد.
هرس کردن و تقطیر
- هرس کردن (Pruning) — حذف وزنهای کماهمیت (بر اساس بزرگی مقدار) یا کانالها (هرس ساختاریافته). روی مدلهای دارای پارامتر مازاد عالی جواب میدهد؛ اما روی معماریهایی که از پیش فشردهاند چندان کارآمد نیست.
- تقطیر (Distillation) — آموزش مدلی کوچک (دانشآموز) برای تقلید از لاجیتهای یک مدل بزرگ (معلم). معمولاً بخش عمدهی دقت ازدسترفته بر اثر کوچکسازی مدل را برمیگرداند و برای مدلهای عملیاتی لبه یک استاندارد به حساب میآید.
محیطهای اجرای استنتاج
- حالت Eager در PyTorch — کند است و به درد استقرار عملیاتی نمیخورد. فقط برای توسعه استفاده کنید.
- TorchScript — قدیمی شده و جای خود را به
torch.compileو خروجی ONNX داده است. - ONNX Runtime — محیط اجرای بیطرف و چندمنظوره. برای سختافزارهای CPU، CUDA، CoreML، TensorRT و OpenVINO پرووایدر دارد. کار را از اینجا شروع کنید.
- TensorRT — کامپایلر اختصاصی NVIDIA. بهترین تاخیر را روی پردازندههای گرافیکی NVIDIA (ورکاستیشن و Jetson) ارائه میدهد. بهصورت مستقل یا ادغامشده در ONNX Runtime کار میکند.
- Core ML — محیط اجرای اختصاصی اپل برای iOS/macOS. به فایلهای
.mlmodelیا.mlpackageنیاز دارد. - TFLite — محیط اجرای گوگل برای Android/ARM. به فایلهای
.tfliteنیاز دارد. - OpenVINO — محیط اجرای اینتل برای CPU و VPU. به فایلهای
.xmlو.binنیاز دارد.
در عمل: از PyTorch خروجی ONNX بگیرید، سپس محیط اجرای مناسب سختافزار مقصد را انتخاب کنید. ONNX زبان مشترک همهی ابزارهاست.
راهنمای انتخاب معماری در لبه
| بودجه | مدل | دلیل انتخاب |
|---|---|---|
| کمتر از 3M پارامتر | MobileNetV3-Small | همهجا کامپایل میشود، بیسلاین مناسبی است |
| 3 تا 10M | EfficientNet-Lite-B0 | بالاترین دقت به ازای هر پارامتر در TFLite |
| 10 تا 20M | ConvNeXt-Tiny | بهترین بازدهی دقت بر پارامتر، بسیار سازگار با CPU |
| 20 تا 30M | MobileViT-S یا EfficientViT | ترنسفورمر با دقت همسطح ImageNet |
| 30 تا 80M | Swin-V2-Tiny | در صورتی که بستر نرمافزاری از اتنشن پنجرهای پشتیبانی کند |
همهی این مدلها را به INT8 کوانتیزه کنید، مگر اینکه دلیل مشخصی برای خلاف آن داشته باشید.
پیادهسازی
گام 1: اندازهگیری اصولی تاخیر
import time
import torch
def measure_latency(model, input_shape, device="cpu", warmup=10, iters=50):
model = model.to(device).eval()
x = torch.randn(input_shape, device=device)
with torch.no_grad():
for _ in range(warmup):
model(x)
if device == "cuda":
torch.cuda.synchronize()
times = []
for _ in range(iters):
if device == "cuda":
torch.cuda.synchronize()
t0 = time.perf_counter()
model(x)
if device == "cuda":
torch.cuda.synchronize()
times.append((time.perf_counter() - t0) * 1000)
times.sort()
return {
"p50_ms": times[len(times) // 2],
"p95_ms": times[int(len(times) * 0.95)],
"p99_ms": times[int(len(times) * 0.99)],
"mean_ms": sum(times) / len(times),
}
مدل را گرم کنید، همگامسازی را انجام دهید و از time.perf_counter() استفاده کنید. صدکها را گزارش دهید، نه صرفاً میانگین را.
گام 2: شمارش پارامترها و FLOPs
def parameter_count(model):
return sum(p.numel() for p in model.parameters())
def flops_estimate(model, input_shape):
"""
Rough FLOP count for a conv/linear-only model. For production use `fvcore` or `ptflops`.
"""
total = 0
def conv_hook(m, inp, out):
nonlocal total
c_out, c_in, kh, kw = m.weight.shape
h, w = out.shape[-2:]
total += 2 * c_in * c_out * kh * kw * h * w
def linear_hook(m, inp, out):
nonlocal total
total += 2 * m.in_features * m.out_features
hooks = []
for m in model.modules():
if isinstance(m, torch.nn.Conv2d):
hooks.append(m.register_forward_hook(conv_hook))
elif isinstance(m, torch.nn.Linear):
hooks.append(m.register_forward_hook(linear_hook))
model.eval()
with torch.no_grad():
model(torch.randn(input_shape))
for h in hooks:
h.remove()
return total
در پروژههای واقعی از fvcore.nn.FlopCountAnalysis یا ptflops استفاده کنید؛ این کتابخانهها انواع ماژولها را بهدرستی پوشش میدهند.
گام 3: کوانتیزهسازی ایستا پس از آموزش
def quantise_ptq(model, calibration_loader, backend="x86"):
import torch.ao.quantization as tq
model = model.eval().cpu()
model.qconfig = tq.get_default_qconfig(backend)
tq.prepare(model, inplace=True)
with torch.no_grad():
for x, _ in calibration_loader:
model(x)
tq.convert(model, inplace=True)
return model
چهار گام اصلی: پیکربندی، آمادهسازی (قراردادن observerها)، کالیبراسیون با دادههای واقعی و در نهایت تبدیل (ادغام لایهها + کوانتیزه کردن). این فرآیند مستلزم ادغام لایههاست (Conv -> BN -> ReLU به ConvBnReLU) که torch.ao.quantization.fuse_modules انجامش میدهد.
گام 4: خروجی گرفتن به فرمت ONNX
def export_onnx(model, sample_input, path="model.onnx"):
model = model.eval()
torch.onnx.export(
model,
sample_input,
path,
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
opset_version=17,
)
return path
نسخهی opset_version=17 پیشفرض امن در سال 2026 است. پارامتر dynamic_axes امکان اجرای مدل ONNX را با اندازههای دلخواه بچ فراهم میکند.
گام 5: بنچمارک و مقایسهی حالتهای مختلف
import torch.nn as nn
from torchvision.models import mobilenet_v3_small
def compare_regimes():
model = mobilenet_v3_small(weights=None, num_classes=10)
params = parameter_count(model)
flops = flops_estimate(model, (1, 3, 224, 224))
lat_fp32 = measure_latency(model, (1, 3, 224, 224), device="cpu")
print(f"FP32 MobileNetV3-Small: {params:,} params {flops/1e9:.2f} GFLOPs "
f"p50={lat_fp32['p50_ms']:.2f}ms p95={lat_fp32['p95_ms']:.2f}ms")
همین تابع را برای resnet50، efficientnet_v2_s و convnext_tiny اجرا کنید تا جدول مقایسهای لازم برای تصمیمگیری استقرار را به دست آورید.
استفادهی عملی
فرایندهای عملیاتی معمولاً به یکی از سه مسیر زیر ختم میشوند:
- وب / بدون سرور (Serverless): مدل PyTorch -> خروجی ONNX -> اجرا در ONNX Runtime (با پرووایدر CPU یا CUDA). سادهترین مسیر و برای اکثر نیازها کاملاً مناسب است.
- پردازش لبه در NVIDIA (بردهای Jetson، سرورهای GPU): مدل PyTorch -> خروجی ONNX -> کامپایل در TensorRT. کمترین تاخیر، بیشترین زحمت مهندسی.
- موبایل: مدل PyTorch -> خروجی ONNX -> تبدیل به Core ML (در iOS) یا TFLite (در اندروید). حتماً قبل از خروجی گرفتن کوانتیزهسازی کنید.
برای تحلیل دقیقتر، ابزارهای torch-tb-profiler، nvprof / nsys و Instruments در macOS جزئیات را لایه به لایه نشان میدهند. ابزارهای خط فرمانی benchmark_app (در OpenVINO) و trtexec (در TensorRT) هم بدون درگیر شدن با کد، آمارهای مستقلی ارائه میکنند.
تحویل پروژه
خروجیهای این درس:
outputs/prompt-edge-deployment-planner.md— پرامپتی که بر اساس سختافزار مقصد و تعهد سطح خدمت (SLA) تاخیر، معماری پایه، استراتژی کوانتیزهسازی و محیط اجرا را انتخاب میکند.outputs/skill-latency-profiler.md— مهارتی که یک اسکریپت کامل برای بنچمارک تاخیر بههمراه گرمکردن، همگامسازی، محاسبهی صدکها و ردیابی حافظه مینویسد.
تمرینها
- (ساده) تاخیر p50 را برای
resnet18،mobilenet_v3_small،efficientnet_v2_sوconvnext_tinyدر ابعاد 224x224 روی CPU بسنجید. جدول نتایج را آماده کنید و ببینید کدام معماری بهترین نسبت دقت به میلیثانیه را دارد. - (متوسط) کوانتیزهسازی ایستای پس از آموزش را روی
mobilenet_v3_smallپیاده کنید. مقایسهی تاخیر و افت دقت بین FP32 و INT8 را روی زیرمجموعهای از دادههای آزمون CIFAR-10 (یا دیتاست مشابه) گزارش دهید. - (سخت) از مدل
convnext_tinyخروجی ONNX بگیرید، آن را درonnxruntimeباCPUExecutionProviderاجرا کنید و تاخیرش را با بیسلاین PyTorch eager بسنجید. اولین لایهای را که در آن ONNX Runtime سریعتر عمل میکند پیدا کنید و دلیلش را توضیح دهید.
اصطلاحات کلیدی
| اصطلاح | اصطلاح رایج | مفهوم واقعی |
|---|---|---|
| تاخیر (Latency) | «چقدر سریع است» | زمان رسیدن ورودی به خروجی؛ بر اساس صدکهای p50/p95/p99، نه میانگین |
| محاسبات ممیز شناور (FLOPs) | «حجم مدل» | عملیات ممیز شناور در هر گذر رو به جلو؛ معیاری تقریبی برای هزینهی محاسباتی |
| کوانتیزهسازی INT8 | «8 بیتی» | جایگزینی وزنها/اکتیویشنهای FP32 با اعداد صحیح 8 بیتی؛ حدوداً 4 برابر کوچکتر، 2 تا 4 برابر سریعتر |
| PTQ | «کوانتیزهسازی بعد از آموزش» | کوانتیزه کردن مدل آموزشدیده بدون بازآموزی؛ ساده و در اغلب موارد کافی |
| QAT | «آموزش آگاه از کوانتیزهسازی» | شبیهسازی اثر کوانتیزهسازی حین آموزش؛ بالاترین دقت، نیازمند دادههای برچسبدار |
| ONNX | «فرمت استاندارد و خنثی» | فرمت تبادل مدل که تمام محیطهای اجرای استنتاج رایج از آن پشتیبانی میکنند |
| TensorRT | «کامپایلر NVIDIA» | ابزاری که مدل ONNX را به یک موتور بهینهشده برای پردازندههای گرافیکی NVIDIA کامپایل میکند |
| تقطیر (Distillation) | «معلم -> دانشآموز» | آموزش مدلی کوچک برای تقلید از لاجیتهای مدلی بزرگ؛ جبرانکنندهی بخش عمدهی افت دقت |
برای مطالعهی بیشتر
- EfficientNet (Tan & Le, 2019) — مقیاسپذیری ترکیبی برای معماریهای بهینه
- MobileNetV3 (Howard et al., 2019) — معماری متمرکز بر موبایل با توابع h-swish و لایههای squeeze-excite
- Accelerating Inference Up to 6x Faster in PyTorch with Torch-TensorRT (NVIDIA) — راهنمای عملی برای دستیابی به ارقام نرخ پردازش ذکرشده در مقالات
- مستندات ONNX Runtime — کوانتیزهسازی، بهینهسازی گراف و انتخاب پرووایدرها
