فهرست مقاله
پیش از اجرای یک مسیر هوش مصنوعی روی سامانهٔ نهفته، باید بدانیم چه دادههایی همزمان در حافظه لازماند و بودجهٔ زمان بر چه فرضی تکیه دارد. در این درس، شکل چند بافر و مرحلههای فرضی را با پایتون حساب میکنیم. هیچ تانسور واقعی تخصیص داده نمیشود، مدل اجرا نمیشود و بردی متصل نیست. نتیجه، محاسبهٔ یک برگهٔ منابع است؛ مصرف RAM، زمان اجرای واقعی یا امکان استقرار محصول را اثبات نمیکند.
۱. اندازهٔ داده را از حافظهٔ کل برنامه جدا کنیم

در معرفی مسیر Cortex-M در ۲۳ مهر ۱۴۰۵، نگهدارندگان PyTorch دربارهٔ تصمیمهای وابسته به هدف، از جمله برنامهریزی حافظه، توضیح میدهند. این گزارش سازنده، زمینهٔ بحث ماست؛ عددهای آزمون سختافزاری آن را بازتولید نکردهایم. کد حاضر به ExecuTorch، TensorFlow Lite Micro یا NumPy وابسته نیست و فقط کتابخانهٔ استاندارد پایتون را اجرا میکند.
اندازهٔ محتوای یک بافر فشرده از حاصلضرب ابعاد در اندازهٔ عنصر به دست میآید. طبق توضیح nbytes در مستندات NumPy، بایت عناصر با تمام حافظهٔ شیء یکسان نیست. در اینجا حتی آرایهٔ NumPy هم نمیسازیم؛ شکلها صرفاً دادهٔ ورودی محاسبهاند. حافظهٔ دیکشنریها و عددهای پایتون روی کامپیوتر میزبان، عدد برآوردشده برای بافر هدف نیست.
در جدول نوع، float32 را چهار بایت و int8 را یک بایت تعریف کردهایم. تابع calcsize در struct پایتون اندازهٔ قالب استاندارد را مشخص میکند؛ از قالب بومیِ وابسته به چیدمان ماشین استفاده نمیکنیم. این شمارش، padding، همترازی آغاز بافر یا سربار ساختارهای زمان اجرا را خودکار اضافه نمیکند. برای بودجهٔ واقعی، این موارد باید با ابزار همان هدف بررسی شوند.
۲. بافرهای همزمان زنده و فضای کمکی را اعلام کنیم

سه بافر ساختگی داریم: input با شکل یک در ۳۲ در ۱۶، hidden با شکل یک در ۶۴ در ۱۶ و output با شکل یک در ۶۴ در ۸. با عنصر چهاربایتی، محتوا به ترتیب ۲۰۴۸، ۴۰۹۶ و ۲۰۴۸ بایت میشود. این شکلها تعریف یک شبکهٔ عصبی کامل نیستند؛ هیچ عملگر یا وابستگی گراف از روی آنها استنتاج نمیشود. نام layer در جدول نیز به معنی اجرای لایهٔ آموختهشده نیست.
در مرحلهٔ prepare فقط input زنده است؛ در layer1، input و hidden و در layer2، hidden و output لازماند. زنده بودن یعنی در همان مرحله به داده نیاز داریم. فضای کمکی یا scratch به ترتیب ۲۵۶، ۱۰۲۴ و ۲۵۶ بایت فرض شده است. ذخیرهٔ ثابت ۸۱۹۲ بایت هم انتخاب مسئله است؛ از اندازهٔ وزنهای مدل، پشته یا کتابخانهای استخراج نشده و کامل بودن پوشش آنها را ادعا نمیکنیم.
مستند مدیریت حافظهٔ TFLM میان تخصیص ماندگار، بافرهای مشترک و فضای کوتاهمدت تفاوت میگذارد. در محاسبهٔ ما، محل قرارگیری بافرها و امکان استفادهٔ مجدد از آدرس تعیین نمیشود. جدول زندهبودن را فراخواننده اعلام میکند؛ اشتباه در آن جدول، میتواند برآورد را کم کند. جمع بایتهای زنده به تنهایی، اندازهٔ کافی arena یا نبود برخورد حافظه را اثبات نمیکند.
۳. کد کامل و بیست بررسی اجراشدهٔ قرارداد بودجه

فایل edge_resource_budget.py را ذخیره و اجرا کنید. اجرای ثبتشده با Python 3.14.5 انجام شده است. ابعاد باید صحیح مثبت باشند؛ فضای کمکی، ذخیرهٔ ثابت و زمان فرضی، صحیح نامنفیاند و bool پذیرفته نمیشود. شناسهٔ بافر ناشناخته یا تکراری در یک مرحله رد میشود تا یک بافر دوبار شمرده نشود. این کنترل شکلی، درستی جدول استخراجشده از مدل واقعی را ثابت نمیکند.
"""Authored resource arithmetic only; no tensor allocation, model or MCU execution."""
import copy
import json
import math
import platform
import struct
FORMATS = {'float32': '<f', 'int8': '<b'}
BUFFERS = {
'input': ((1, 32, 16), 'float32'),
'hidden': ((1, 64, 16), 'float32'),
'output': ((1, 64, 8), 'float32'),
}
STAGES = (
{'name': 'prepare', 'live': ('input',), 'scratch_bytes': 256, 'assumed_us': 2000},
{'name': 'layer1', 'live': ('input', 'hidden'), 'scratch_bytes': 1024, 'assumed_us': 3000},
{'name': 'layer2', 'live': ('hidden', 'output'), 'scratch_bytes': 256, 'assumed_us': 1000},
)
def nonnegative_int(value, field, minimum=0):
if type(value) is not int or value < minimum:
raise ValueError(field + ' must be an exact Python int at least ' + str(minimum))
def identifier(value):
if type(value) is not str or not value or any(ch.isspace() for ch in value):
raise ValueError('Nonempty exact string identifier without whitespace required')
def payload_bytes(shape, kind):
if type(shape) is not tuple or not shape:
raise ValueError('A nonempty tuple of dimensions is required')
for dimension in shape:
nonnegative_int(dimension, 'dimension', 1)
if type(kind) is not str or kind not in FORMATS:
raise ValueError('Only the declared float32 and int8 packed representations are supported')
return math.prod(shape) * struct.calcsize(FORMATS[kind])
def estimate(buffers, stages, *, reserved_bytes=8192, ram_limit_bytes=16384, deadline_us=6000):
nonnegative_int(reserved_bytes, 'reserved_bytes')
nonnegative_int(ram_limit_bytes, 'ram_limit_bytes', 1)
nonnegative_int(deadline_us, 'deadline_us', 1)
if type(buffers) is not dict or not buffers:
raise ValueError('Nonempty buffer dictionary required')
sizes = {}
for name, specification in buffers.items():
identifier(name)
if type(specification) is not tuple or len(specification) != 2:
raise ValueError('Each buffer specification must be (shape_tuple, kind)')
sizes[name] = payload_bytes(*specification)
if type(stages) not in (tuple, list) or not stages:
raise ValueError('Nonempty ordered stages required')
reports, stage_names = [], set()
for stage in stages:
if type(stage) is not dict or set(stage) != {'name', 'live', 'scratch_bytes', 'assumed_us'}:
raise ValueError('Exact stage fields are required')
identifier(stage['name'])
if stage['name'] in stage_names:
raise ValueError('Stage names must be unique')
stage_names.add(stage['name'])
live = stage['live']
if type(live) is not tuple or not live:
raise ValueError('Each stage declares a nonempty live-buffer tuple')
for name in live:
identifier(name)
if name not in sizes:
raise ValueError('Unknown live-buffer ID')
if len(set(live)) != len(live):
raise ValueError('A live buffer must not be counted twice')
nonnegative_int(stage['scratch_bytes'], 'scratch_bytes')
nonnegative_int(stage['assumed_us'], 'assumed_us')
live_payload = sum(sizes[name] for name in live)
reports.append({'name': stage['name'], 'live': list(live), 'live_payload_bytes': live_payload,
'scratch_bytes': stage['scratch_bytes'],
'estimated_bytes': reserved_bytes + live_payload + stage['scratch_bytes'],
'assumed_us': stage['assumed_us']})
peak = max(reports, key=lambda row: row['estimated_bytes']) # First stage wins an exact tie.
assumed_total_us = sum(row['assumed_us'] for row in reports)
return {'buffer_payload_bytes': sizes, 'reserved_bytes': reserved_bytes, 'stages': reports,
'peak_modeled_bytes': peak['estimated_bytes'], 'peak_stage': peak['name'],
'peak_modeled_kib': peak['estimated_bytes'] / 1024,
'ram_limit_bytes': ram_limit_bytes, 'ram_margin_bytes': ram_limit_bytes - peak['estimated_bytes'],
'fits_ram_hypothesis': peak['estimated_bytes'] <= ram_limit_bytes,
'all_buffers_resident_plus_max_scratch_bytes': reserved_bytes + sum(sizes.values()) + max(row['scratch_bytes'] for row in reports),
'assumed_serial_total_us': assumed_total_us, 'deadline_us': deadline_us,
'latency_margin_us': deadline_us - assumed_total_us,
'fits_latency_hypothesis': assumed_total_us <= deadline_us}
def rejects(call):
try:
call()
except ValueError:
return True
return False
def run_example():
original_buffers, original_stages = copy.deepcopy(BUFFERS), copy.deepcopy(STAGES)
baseline = estimate(BUFFERS, STAGES)
enlarged_scratch = copy.deepcopy(STAGES)
enlarged_scratch[2]['scratch_bytes'] = 4000
scratch_report = estimate(BUFFERS, enlarged_scratch)
tie_stages = STAGES + ({**STAGES[1], 'name': 'same_peak_later'},)
checks = {
'all_payload_sizes_match_independent_hand_products': baseline['buffer_payload_bytes'] == {'input': 2048, 'hidden': 4096, 'output': 2048},
'packed_element_sizes_are_four_and_one_bytes': struct.calcsize('<f') == 4 and struct.calcsize('<b') == 1,
'three_stage_totals_match_hand_reference': [row['estimated_bytes'] for row in baseline['stages']] == [10496, 15360, 14592],
'peak_is_layer1_not_sum_of_stage_totals': baseline['peak_modeled_bytes'] == 15360 and baseline['peak_stage'] == 'layer1',
'kib_uses_1024_bytes': baseline['peak_modeled_kib'] == 15.0 and baseline['ram_limit_bytes'] == 16 * 1024,
'ram_margin_is_one_kib_under_authored_assumptions': baseline['fits_ram_hypothesis'] is True and baseline['ram_margin_bytes'] == 1024,
'serial_latency_is_six_thousand_assumed_microseconds': baseline['assumed_serial_total_us'] == 6000 and baseline['fits_latency_hypothesis'] is True and baseline['latency_margin_us'] == 0,
'ram_equality_is_accepted': estimate(BUFFERS, STAGES, ram_limit_bytes=15360)['fits_ram_hypothesis'] is True,
'ram_one_byte_short_is_rejected_by_budget_comparison': estimate(BUFFERS, STAGES, ram_limit_bytes=15359)['fits_ram_hypothesis'] is False and estimate(BUFFERS, STAGES, ram_limit_bytes=15359)['ram_margin_bytes'] == -1,
'deadline_one_microsecond_short_is_over_budget': estimate(BUFFERS, STAGES, deadline_us=5999)['fits_latency_hypothesis'] is False and estimate(BUFFERS, STAGES, deadline_us=5999)['latency_margin_us'] == -1,
'all_resident_assumption_is_seventeen_kib_and_differs': baseline['all_buffers_resident_plus_max_scratch_bytes'] == 17 * 1024 > baseline['ram_limit_bytes'] > baseline['peak_modeled_bytes'],
'int8_payload_count_is_not_a_quantization_execution': payload_bytes((1, 32, 16), 'int8') == 512 and payload_bytes((1, 32, 16), 'float32') == 4 * 512,
'larger_scratch_moves_peak_and_exceeds_capacity': scratch_report['peak_stage'] == 'layer2' and scratch_report['peak_modeled_bytes'] == 18336 and scratch_report['fits_ram_hypothesis'] is False,
'fixed_reserve_addition_shifts_peak_by_same_bytes': estimate(BUFFERS, STAGES, reserved_bytes=8448)['peak_modeled_bytes'] == baseline['peak_modeled_bytes'] + 256,
'buffer_dictionary_order_does_not_change_budget': estimate(dict(reversed(list(BUFFERS.items()))), STAGES) == baseline,
'invalid_shapes_and_unknown_representations_rejected': all(rejects(lambda shape=shape: payload_bytes(shape, 'float32')) for shape in ((), (0, 4), (-1, 4), (True, 4), (1.5, 4), [1, 4])) and rejects(lambda: payload_bytes((1, 4), 'float16')),
'bad_live_ids_duplicates_and_negative_scratch_rejected': all((rejects(lambda: estimate(BUFFERS, ({**STAGES[0], 'live': ('unknown',)},))), rejects(lambda: estimate(BUFFERS, ({**STAGES[0], 'live': ('input', 'input')},))), rejects(lambda: estimate(BUFFERS, ({**STAGES[0], 'scratch_bytes': -1},))), rejects(lambda: estimate(BUFFERS, ({**STAGES[0], 'assumed_us': True},))))),
'bad_limits_reserve_empty_catalog_and_empty_stages_rejected': all((rejects(lambda: estimate(BUFFERS, STAGES, ram_limit_bytes=0)), rejects(lambda: estimate(BUFFERS, STAGES, deadline_us=0)), rejects(lambda: estimate(BUFFERS, STAGES, reserved_bytes=-1)), rejects(lambda: estimate(BUFFERS, STAGES, deadline_us=6000.0)), rejects(lambda: estimate({}, STAGES)), rejects(lambda: estimate(BUFFERS, ())))),
'input_buffer_and_stage_records_not_mutated': BUFFERS == original_buffers and STAGES == original_stages,
'exact_peak_tie_keeps_first_stage': estimate(BUFFERS, tie_stages)['peak_stage'] == 'layer1',
}
assert len(checks) == 20 and all(value is True for value in checks.values()), checks
return {'python': platform.python_version(), 'scope': 'synthetic_declared_resource_budget_arithmetic',
'byte_contract': 'packed element payload only; no array allocation or allocator placement',
'latency_contract': 'authored serial stage assumptions, not elapsed measurements or WCET',
'baseline': baseline, 'larger_scratch_scenario': scratch_report,
'model_mcu_framework_resampler_or_quantization_executed': False, 'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
برای هر مرحله، ذخیرهٔ ثابت، محتوای بافرهای زنده و scratch را جمع میکنیم؛ سپس بیشینه را برمیداریم. در تساوی دقیق، نخستین مرحله در گزارش باقی میماند. بیست بررسی، حاصلضربهای دستی، مجموع مرحلهها، مرز مساوی ظرفیت و حالت یک بایت کمتر را میآزمایند. تغییر ترتیب واژهنامهٔ بافرها نباید نتیجهٔ بودجه را عوض کند و ورودیها نیز با تصویر مستقل پیش از اجرا مقایسه میشوند.
هزینهٔ زمانی با میکروثانیهٔ صحیح نگه داشته میشود تا مقایسهٔ این نمونه به گردکردن اعشاری وابسته نباشد. مرحلهها را سری فرض کردهایم و زمانهای تألیفیِ ۲۰۰۰، ۳۰۰۰ و ۱۰۰۰ را جمع میکنیم. هیچ ساعت، sleep یا پروفایلگیری وجود ندارد. برای مدل موازی، صف، وقفه یا انتقال DMA، این جمع ساده لزوماً زمان پاسخ را نشان نمیدهد؛ بدترین زمان اجرا هم اندازهگیری یا اثبات نشده است.
۴. برآورد مرحلهای، فرض اقامت کامل و مرز ظرفیت

مجموع سه مرحله به ترتیب ۱۰۴۹۶، ۱۵۳۶۰ و ۱۴۵۹۲ بایت است. بیشینهٔ محاسبه، ۱۵۳۶۰ بایت در layer1 یا ۱۵ کیبیبایت است. هر KiB برابر ۱۰۲۴ بایت است؛ ظرفیت فرضی ۱۶ KiB، حاشیهٔ ۱۰۲۴ بایت میدهد. این حاشیه فقط در چارچوب اقلام اعلامشده وجود دارد؛ هزینهٔ فراموششدهٔ پشته، همترازی یا بافر ورودی میتواند آن را مصرف کند.
{
"python": "3.14.5",
"scope": "synthetic_declared_resource_budget_arithmetic",
"byte_contract": "packed element payload only; no array allocation or allocator placement",
"latency_contract": "authored serial stage assumptions, not elapsed measurements or WCET",
"baseline": {
"buffer_payload_bytes": {
"input": 2048,
"hidden": 4096,
"output": 2048
},
"reserved_bytes": 8192,
"stages": [
{
"name": "prepare",
"live": [
"input"
],
"live_payload_bytes": 2048,
"scratch_bytes": 256,
"estimated_bytes": 10496,
"assumed_us": 2000
},
{
"name": "layer1",
"live": [
"input",
"hidden"
],
"live_payload_bytes": 6144,
"scratch_bytes": 1024,
"estimated_bytes": 15360,
"assumed_us": 3000
},
{
"name": "layer2",
"live": [
"hidden",
"output"
],
"live_payload_bytes": 6144,
"scratch_bytes": 256,
"estimated_bytes": 14592,
"assumed_us": 1000
}
],
"peak_modeled_bytes": 15360,
"peak_stage": "layer1",
"peak_modeled_kib": 15.0,
"ram_limit_bytes": 16384,
"ram_margin_bytes": 1024,
"fits_ram_hypothesis": true,
"all_buffers_resident_plus_max_scratch_bytes": 17408,
"assumed_serial_total_us": 6000,
"deadline_us": 6000,
"latency_margin_us": 0,
"fits_latency_hypothesis": true
},
"larger_scratch_scenario": {
"buffer_payload_bytes": {
"input": 2048,
"hidden": 4096,
"output": 2048
},
"reserved_bytes": 8192,
"stages": [
{
"name": "prepare",
"live": [
"input"
],
"live_payload_bytes": 2048,
"scratch_bytes": 256,
"estimated_bytes": 10496,
"assumed_us": 2000
},
{
"name": "layer1",
"live": [
"input",
"hidden"
],
"live_payload_bytes": 6144,
"scratch_bytes": 1024,
"estimated_bytes": 15360,
"assumed_us": 3000
},
{
"name": "layer2",
"live": [
"hidden",
"output"
],
"live_payload_bytes": 6144,
"scratch_bytes": 4000,
"estimated_bytes": 18336,
"assumed_us": 1000
}
],
"peak_modeled_bytes": 18336,
"peak_stage": "layer2",
"peak_modeled_kib": 17.90625,
"ram_limit_bytes": 16384,
"ram_margin_bytes": -1952,
"fits_ram_hypothesis": false,
"all_buffers_resident_plus_max_scratch_bytes": 20384,
"assumed_serial_total_us": 6000,
"deadline_us": 6000,
"latency_margin_us": 0,
"fits_latency_hypothesis": true
},
"model_mcu_framework_resampler_or_quantization_executed": false,
"checks": {
"all_payload_sizes_match_independent_hand_products": true,
"packed_element_sizes_are_four_and_one_bytes": true,
"three_stage_totals_match_hand_reference": true,
"peak_is_layer1_not_sum_of_stage_totals": true,
"kib_uses_1024_bytes": true,
"ram_margin_is_one_kib_under_authored_assumptions": true,
"serial_latency_is_six_thousand_assumed_microseconds": true,
"ram_equality_is_accepted": true,
"ram_one_byte_short_is_rejected_by_budget_comparison": true,
"deadline_one_microsecond_short_is_over_budget": true,
"all_resident_assumption_is_seventeen_kib_and_differs": true,
"int8_payload_count_is_not_a_quantization_execution": true,
"larger_scratch_moves_peak_and_exceeds_capacity": true,
"fixed_reserve_addition_shifts_peak_by_same_bytes": true,
"buffer_dictionary_order_does_not_change_budget": true,
"invalid_shapes_and_unknown_representations_rejected": true,
"bad_live_ids_duplicates_and_negative_scratch_rejected": true,
"bad_limits_reserve_empty_catalog_and_empty_stages_rejected": true,
"input_buffer_and_stage_records_not_mutated": true,
"exact_peak_tie_keeps_first_stage": true
}
}
اگر همهٔ بافرها را همزمان مقیم بگیریم و بزرگترین scratch را هم اضافه کنیم، نتیجه ۱۷۴۰۸ بایت یا ۱۷ KiB میشود. این دو عدد پاسخ دو فرض متفاوتاند؛ جمع کامل لزوماً خطا نیست و برآورد مرحلهای نیز تضمین استفادهٔ مجدد حافظه نیست. در سناریوی دیگر، scratch مرحلهٔ آخر را ۴۰۰۰ میگذاریم؛ قله به ۱۸۳۳۶ بایت میرود و از ظرفیت فرضی عبور میکند، هرچند شکل بافرها تغییر نکرده است.
زمان فرضی سری، ۶۰۰۰ میکروثانیه یا شش میلیثانیه است. با مهلت ۶۰۰۰، مقایسهٔ کوچکتر یا مساوی برقرار است؛ با ۵۹۹۹، یک میکروثانیه کسری داریم. صفر بودن حاشیهٔ این محاسبه، ضمانت مهلت واقعی نیست. راهنمای هزینه و تأخیر استقرار دامنهٔ گستردهتر سنجش را توضیح میدهد؛ این مثال بودجهٔ شبکه، انرژی یا هزینهٔ پولی را مدل نکرده است.
۵. چهار تمرین پاسخدار پیش از آزمون روی هدف

تمرین اول: محتوای input چگونه ۲۰۴۸ بایت میشود؟ پاسخ: حاصلضرب یک، ۳۲ و ۱۶ برابر ۵۱۲ عنصر است و هر عنصر چهار بایت دارد. تعداد عناصر با بایت برابر نیست؛ شکل بدون دانستن نوع، بودجه را کامل نمیکند.
تمرین دوم: آیا ظرفیت دقیقاً ۱۵۳۶۰ بایت در این کد رد میشود؟ پاسخ: خیر؛ قرارداد تساوی را میپذیرد. ظرفیت ۱۵۳۵۹ یک بایت کم دارد. پذیرش محاسبه، فقط مقایسهٔ اقلام فرضی است و تأیید عملی روی برد نیست.
تمرین سوم: چرا ۱۷ KiB با ۱۵ KiB فرق دارد؟ پاسخ: در فرض نخست همهٔ محتواها مقیماند؛ در جدول مرحلهای، input و output همزمان لازم اعلام نشدهاند. اثبات امکان چیدمان و استفادهٔ مجدد، به برنامهریز حافظه و آزمون هدف نیاز دارد.
تمرین چهارم: آیا شمردن int8 به جای float32 یعنی مدل کوانتیزه شده و کیفیت ثابت مانده است؟ پاسخ: خیر؛ تنها محتوای همان شکل از ۲۰۴۸ به ۵۱۲ بایت تغییر میکند. کل RAM الزاماً یکچهارم نمیشود و کالیبراسیون یا دقت مدل بررسی نشده است. قرارداد آرایهٔ صوت نیز نشان میدهد نوع و چینش باید جدا روشن باشند. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدهاند؛ هیچ چارچوب یا مدل نهفتهای در این تمرین اجرا نشده است.
منابع و مطالعهٔ بیشتر
- PyTorch؛ معرفی مسیر ExecuTorch برای پردازندههای Cortex-M، ۱۵ اکتبر ۲۰۲۶
- TensorFlow Lite Micro؛ مدیریت arena، بافرهای مشترک و تخصیص ماندگار
- NumPy 2.4؛ تفاوت nbytes عناصر با حافظهٔ کل شیء
- Python 3.14؛ اندازهٔ قالبهای استاندارد در struct و calcsize
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.


