ذخیره سازی کامپایل

از اندروید 10، API شبکه های عصبی (NNAPI) عملکردهایی را برای پشتیبانی از ذخیره سازی مصنوعات کامپایل ارائه می دهد که زمان شروع برنامه را کاهش می دهد. با استفاده از این قابلیت کش، درایور نیازی به مدیریت یا پاکسازی فایل های کش ندارد. این یک ویژگی اختیاری است که می تواند با NN HAL 1.2 پیاده سازی شود. برای اطلاعات بیشتر در مورد این تابع، به ANeuralNetworksCompilation_setCaching مراجعه کنید.

درایور همچنین می تواند ذخیره سازی کامپایل را مستقل از NNAPI پیاده سازی کند. این را می توان پیاده سازی کرد چه از ویژگی های کش NNAPI NDK و HAL استفاده شود یا نه. AOSP یک کتابخانه ابزار سطح پایین (موتور کش) فراهم می کند. برای اطلاعات بیشتر، به پیاده سازی یک موتور حافظه پنهان مراجعه کنید.

مروری بر گردش کار

این بخش گردش های کاری کلی را با ویژگی ذخیره سازی کامپایل پیاده سازی شده توضیح می دهد.

اطلاعات کش ارائه شده و ضربه کش

  1. برنامه یک دایرکتوری ذخیره سازی و یک جمع کنترلی منحصر به فرد برای مدل ارسال می کند.
  2. زمان اجرا NNAPI به دنبال فایل های کش بر اساس جمع کنترل، اولویت اجرا و نتیجه پارتیشن بندی می گردد و فایل ها را پیدا می کند.
  3. NNAPI فایل‌های کش را باز می‌کند و دسته‌ها را با prepareModelFromCache به درایور منتقل می‌کند.
  4. درایور مدل را مستقیماً از فایل های کش آماده می کند و مدل آماده شده را برمی گرداند.

اطلاعات کش ارائه شده و از دست رفتن حافظه پنهان

  1. این برنامه یک چک‌جمع منحصر به فرد برای مدل و یک فهرست ذخیره‌سازی را ارسال می‌کند.
  2. زمان اجرا NNAPI به دنبال فایل‌های ذخیره‌سازی بر اساس جمع‌بندی، اولویت اجرا و نتیجه پارتیشن‌بندی می‌گردد و فایل‌های کش را پیدا نمی‌کند.
  3. NNAPI فایل‌های کش خالی را بر اساس چک‌جمع، اولویت اجرا و پارتیشن‌بندی ایجاد می‌کند، فایل‌های کش را باز می‌کند و دسته‌ها و مدل را با prepareModel_1_2 به درایور می‌دهد.
  4. درایور مدل را کامپایل می کند، اطلاعات کش را در فایل های کش می نویسد و مدل آماده شده را برمی گرداند.

اطلاعات حافظه پنهان ارائه نشده است

  1. برنامه کامپایل را بدون ارائه هیچ گونه اطلاعات ذخیره سازی فراخوانی می کند.
  2. برنامه هیچ چیز مربوط به کش را ارسال نمی کند.
  3. زمان اجرا NNAPI مدل را با prepareModel_1_2 به درایور ارسال می کند.
  4. درایور مدل را کامپایل کرده و مدل آماده شده را برمی گرداند.

اطلاعات کش

اطلاعات ذخیره‌سازی که در اختیار راننده قرار می‌گیرد شامل یک نشانه و دسته‌های فایل کش است.

رمز

توکن یک رمز ذخیره با طول Constant::BYTE_SIZE_OF_CACHE_TOKEN است که مدل آماده شده را شناسایی می کند. هنگام ذخیره فایل‌های کش با prepareModel_1_2 و بازیابی مدل آماده‌شده با prepareModelFromCache همین توکن ارائه می‌شود. مشتری راننده باید توکنی با نرخ برخورد کم انتخاب کند. راننده نمی تواند برخورد نشانه ای را تشخیص دهد. یک برخورد منجر به اجرای ناموفق یا اجرای موفقیت آمیز می شود که مقادیر خروجی نادرستی ایجاد می کند.

دسته فایل های کش (دو نوع فایل کش)

دو نوع فایل کش عبارتند از کش داده و کش مدل .

  • حافظه پنهان داده: برای ذخیره سازی داده های ثابت از جمله بافرهای تانسور پیش پردازش شده و تبدیل شده استفاده می شود. تغییر در حافظه نهان داده ها نباید به هیچ اثری بدتر از ایجاد مقادیر بد خروجی در زمان اجرا منجر شود.
  • حافظه پنهان مدل: برای ذخیره داده های حساس به امنیت مانند کد ماشین اجرایی کامپایل شده در قالب باینری بومی دستگاه استفاده می شود. تغییر در حافظه پنهان مدل ممکن است بر رفتار اجرای درایور تأثیر بگذارد و یک کلاینت مخرب می تواند از آن برای اجرای فراتر از مجوز اعطا شده استفاده کند. بنابراین، درایور باید قبل از تهیه مدل از حافظه پنهان بررسی کند که آیا حافظه پنهان مدل خراب است یا خیر. برای اطلاعات بیشتر، امنیت را ببینید.

درایور باید تصمیم بگیرد که اطلاعات حافظه نهان چگونه بین دو نوع فایل کش توزیع شود و با getNumberOfCacheFilesNeeded به تعداد فایل های کش برای هر نوع گزارش دهد.

زمان اجرا NNAPI همیشه دسته های فایل کش را با مجوز خواندن و نوشتن باز می کند.

امنیت

در کش کامپایل، حافظه پنهان مدل ممکن است حاوی داده های حساس به امنیت مانند کد ماشین اجرایی کامپایل شده در قالب باینری بومی دستگاه باشد. اگر به درستی محافظت نشود، تغییر در حافظه پنهان مدل ممکن است بر رفتار اجرای درایور تأثیر بگذارد. از آنجایی که محتویات کش در دایرکتوری برنامه ذخیره می شوند، فایل های کش توسط مشتری قابل تغییر هستند. یک کلاینت حشره دار ممکن است به طور تصادفی حافظه پنهان را خراب کند و یک کلاینت مخرب می تواند عمداً از آن برای اجرای کد تأیید نشده در دستگاه استفاده کند. بسته به ویژگی های دستگاه، این ممکن است یک مشکل امنیتی باشد. بنابراین، درایور باید بتواند قبل از آماده‌سازی مدل از حافظه پنهان، خرابی حافظه پنهان مدل را شناسایی کند.

یکی از راه‌های انجام این کار این است که راننده یک نقشه را از توکن به یک هش رمزنگاری از حافظه پنهان مدل نگهداری کند. درایور می تواند رمز و هش حافظه پنهان مدل خود را هنگام ذخیره کامپایل در حافظه پنهان ذخیره کند. درایور هنگام بازیابی کامپایل از حافظه نهان، هش جدید حافظه پنهان مدل را با توکن ضبط شده و جفت هش بررسی می کند. این نقشه برداری باید در سراسر راه اندازی مجدد سیستم پایدار باشد. درایور می‌تواند از سرویس ذخیره کلید اندروید ، کتابخانه ابزار در framework/ml/nn/driver/cache یا هر مکانیسم مناسب دیگری برای پیاده‌سازی مدیر نقشه‌برداری استفاده کند. پس از به‌روزرسانی درایور، این مدیر نقشه‌برداری باید مجدداً راه‌اندازی شود تا از تهیه فایل‌های کش از نسخه‌های قبلی جلوگیری شود.

برای جلوگیری از حملات زمان بررسی تا زمان استفاده (TOCTOU)، راننده باید هش ضبط شده را قبل از ذخیره در فایل محاسبه کند و پس از کپی محتوای فایل در بافر داخلی، هش جدید را محاسبه کند.

این کد نمونه نحوه پیاده سازی این منطق را نشان می دهد.

bool saveToCache(const sp<V1_2::IPreparedModel> preparedModel,
                 const hidl_vec<hidl_handle>& modelFds, const hidl_vec<hidl_handle>& dataFds,
                 const HidlToken& token) {
    // Serialize the prepared model to internal buffers.
    auto buffers = serialize(preparedModel);

    // This implementation detail is important: the cache hash must be computed from internal
    // buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
    auto hash = computeHash(buffers);

    // Store the {token, hash} pair to a mapping manager that is persistent across reboots.
    CacheManager::get()->store(token, hash);

    // Write the cache contents from internal buffers to cache files.
    return writeToFds(buffers, modelFds, dataFds);
}

sp<V1_2::IPreparedModel> prepareFromCache(const hidl_vec<hidl_handle>& modelFds,
                                          const hidl_vec<hidl_handle>& dataFds,
                                          const HidlToken& token) {
    // Copy the cache contents from cache files to internal buffers.
    auto buffers = readFromFds(modelFds, dataFds);

    // This implementation detail is important: the cache hash must be computed from internal
    // buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
    auto hash = computeHash(buffers);

    // Validate the {token, hash} pair by a mapping manager that is persistent across reboots.
    if (CacheManager::get()->validate(token, hash)) {
        // Retrieve the prepared model from internal buffers.
        return deserialize<V1_2::IPreparedModel>(buffers);
    } else {
        return nullptr;
    }
}

موارد استفاده پیشرفته

در برخی موارد استفاده پیشرفته، یک درایور نیاز به دسترسی به محتوای حافظه پنهان (خواندن یا نوشتن) پس از تماس کامپایل دارد. موارد استفاده نمونه عبارتند از:

  • کامپایل به موقع: کامپایل تا اولین اجرا به تاخیر می افتد.
  • کامپایل چند مرحله ای: یک کامپایل سریع در ابتدا انجام می شود و یک کامپایل بهینه اختیاری در زمان بعدی بسته به دفعات استفاده انجام می شود.

برای دسترسی به محتوای حافظه پنهان (خواندن یا نوشتن) پس از تماس کامپایل، مطمئن شوید که درایور:

  • دسته‌های فایل را در طول فراخوانی prepareModel_1_2 یا prepareModelFromCache کپی می‌کند و محتوای کش را در زمان دیگری می‌خواند/به‌روزرسانی می‌کند.
  • منطق قفل کردن فایل را خارج از فراخوانی کامپایل معمولی پیاده سازی می کند تا از نوشتن همزمان با خواندن یا نوشتن دیگر جلوگیری کند.

یک موتور کش را پیاده سازی کنید

علاوه بر رابط ذخیره سازی کامپایل NN HAL 1.2، می توانید یک کتابخانه ابزار ذخیره سازی را نیز در دایرکتوری frameworks/ml/nn/driver/cache بیابید. دایرکتوری فرعی nnCache حاوی کدهای ذخیره سازی دائمی برای درایور برای پیاده سازی کش کامپایل بدون استفاده از ویژگی های کش NNAPI است. این شکل از کش کامپایل را می توان با هر نسخه ای از NN HAL پیاده سازی کرد. اگر درایور تصمیم بگیرد که حافظه پنهان جدا شده از رابط HAL را پیاده سازی کند، درایور مسئول آزاد کردن مصنوعات کش شده در زمانی که دیگر مورد نیاز نیست، است.