การแคชการคอมไพล์

ตั้งแต่ Android 10 เป็นต้นไป Neural Networks 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 ไคลเอ็นต์ของไดรเวอร์ควรเลือกโทเค็นที่มีอัตราการชนต่ำ ไดรเวอร์ตรวจหาการชนของโทเค็นไม่ได้ การชนจะส่งผลให้การดำเนินการล้มเหลวหรือดำเนินการสำเร็จแต่แสดงค่าเอาต์พุตที่ไม่ถูกต้อง

แฮนเดิลไฟล์แคช (ไฟล์แคช 2 ประเภท)

ไฟล์แคชมี 2 ประเภท ได้แก่ แคชข้อมูลและ แคชโมเดล

  • แคชข้อมูล: ใช้สำหรับแคชข้อมูลคงที่ รวมถึงบัฟเฟอร์เทนเซอร์ที่ประมวลผลล่วงหน้าและแปลงแล้ว การแก้ไขแคชข้อมูลไม่ควรส่งผลเสียมากกว่าการสร้างค่าเอาต์พุตที่ไม่ถูกต้องในเวลาดำเนินการ
  • แคชโมเดล: ใช้สำหรับแคชข้อมูลที่ละเอียดอ่อนด้านความปลอดภัย เช่น โค้ดเครื่องที่คอมไพล์แล้วและดำเนินการได้ในรูปแบบไบนารีเนทีฟของอุปกรณ์ การแก้ไขแคชโมเดลอาจส่งผลต่อลักษณะการทำงานในการดำเนินการของไดรเวอร์ และไคลเอ็นต์ที่เป็นอันตรายอาจใช้ประโยชน์จากสิ่งนี้เพื่อดำเนินการนอกเหนือจากสิทธิ์ที่ได้รับ ดังนั้น ไดรเวอร์ต้องตรวจสอบว่าแคชโมเดลเสียหายหรือไม่ก่อนที่จะเตรียมโมเดลจากแคช ดูข้อมูลเพิ่มเติมได้ที่ หัวข้อความปลอดภัย

ไดรเวอร์ต้องตัดสินใจว่าจะกระจายข้อมูลแคชระหว่างไฟล์แคช 2 ประเภทอย่างไร และรายงานจำนวนไฟล์แคชที่ต้องการสำหรับแต่ละประเภท ด้วย getNumberOfCacheFilesNeeded

รันไทม์ NNAPI จะเปิดแฮนเดิลไฟล์แคชด้วยสิทธิ์อ่านและเขียนเสมอ

ความปลอดภัย

ในการแคชการคอมไพล์ แคชโมเดลอาจมีข้อมูลที่ละเอียดอ่อนด้านความปลอดภัย เช่น โค้ดเครื่องที่คอมไพล์แล้วและดำเนินการได้ในรูปแบบไบนารีเนทีฟของอุปกรณ์ หากไม่ได้รับการปกป้องอย่างเหมาะสม การแก้ไขแคชโมเดลอาจส่งผลต่อลักษณะการทำงานในการดำเนินการของไดรเวอร์ เนื่องจากระบบจะจัดเก็บเนื้อหาแคชไว้ในไดเรกทอรีแอป ไคลเอ็นต์จึงแก้ไขไฟล์แคชได้ ไคลเอ็นต์ที่มีข้อบกพร่องอาจทำให้แคชเสียหายโดยไม่ตั้งใจ และไคลเอ็นต์ที่เป็นอันตรายอาจใช้ประโยชน์จากสิ่งนี้เพื่อดำเนินการโค้ดที่ไม่ได้รับการยืนยันในอุปกรณ์โดยเจตนา ซึ่งอาจเป็นปัญหาด้านความปลอดภัย ทั้งนี้ขึ้นอยู่กับลักษณะของอุปกรณ์ ดังนั้น ไดรเวอร์ต้องตรวจหาการเสียหายที่อาจเกิดขึ้นกับแคชโมเดลได้ก่อนที่จะเตรียมโมเดลจากแคช

วิธีหนึ่งที่ทำได้คือให้ไดรเวอร์เก็บรักษาการแมปจากโทเค็นไปยังแฮชการเข้ารหัสลับของแคชโมเดล ไดรเวอร์สามารถจัดเก็บโทเค็นและแฮชของแคชโมเดลเมื่อบันทึกการคอมไพล์ลงในแคช ไดรเวอร์จะตรวจสอบแฮชใหม่ของแคชโมเดลกับคู่โทเค็นและแฮชที่บันทึกไว้เมื่อดึงข้อมูลการคอมไพล์จากแคช การแมปนี้ควรคงอยู่ตลอดการรีบูตระบบ ไดรเวอร์สามารถใช้ บริการที่เก็บคีย์ Android, ไลบรารียูทิลิตีใน framework/ml/nn/driver/cache, หรือกลไกอื่นๆ ที่เหมาะสมเพื่อใช้ตัวจัดการการแมป เมื่ออัปเดตไดรเวอร์แล้ว ควรเริ่มต้นตัวจัดการการแมปนี้ใหม่เพื่อป้องกันไม่ให้เตรียมไฟล์แคชจากเวอร์ชันก่อนหน้า

ไดรเวอร์ต้องคำนวณแฮชที่บันทึกไว้ก่อนที่จะบันทึกลงใน ไฟล์ และคำนวณแฮชใหม่หลังจากคัดลอกเนื้อหาไฟล์ลงในบัฟเฟอร์ภายใน เพื่อป้องกันการโจมตีแบบ TOCTOU (Time-of-check to time-of-use)

โค้ดตัวอย่างนี้แสดงวิธีใช้ตรรกะนี้

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;
    }
}

Use Case ขั้นสูง

ใน Use Case ขั้นสูงบางกรณี ไดรเวอร์ต้องเข้าถึงเนื้อหาแคช (อ่านหรือเขียน) หลังจากเรียกใช้การคอมไพล์ ตัวอย่าง Use Case ได้แก่

  • การคอมไพล์แบบทันเวลา: การคอมไพล์จะล่าช้าจนกว่าจะมีการดำเนินการครั้งแรก
  • การคอมไพล์แบบหลายขั้นตอน: ระบบจะทำการคอมไพล์อย่างรวดเร็วในตอนแรก และจะทำการคอมไพล์ที่ปรับให้เหมาะสม (ไม่บังคับ) ในภายหลัง ทั้งนี้ขึ้นอยู่กับความถี่ในการใช้งาน

หากต้องการเข้าถึงเนื้อหาแคช (อ่านหรือเขียน) หลังจากเรียกใช้การคอมไพล์ ให้ตรวจสอบว่าไดรเวอร์ทำสิ่งต่อไปนี้

  • ทำซ้ำแฮนเดิลไฟล์ระหว่างการเรียกใช้ prepareModel_1_2 หรือ prepareModelFromCache และอ่าน/อัปเดตเนื้อหาแคชในภายหลัง
  • ใช้ตรรกะการล็อกไฟล์นอกการเรียกใช้การคอมไพล์ปกติเพื่อป้องกันไม่ให้การเขียนเกิดขึ้นพร้อมกับการอ่านหรือการเขียนอื่นๆ

ใช้เอ็นจินการแคช

นอกจากอินเทอร์เฟซการแคชการคอมไพล์ NN HAL 1.2 แล้ว คุณยังดูไลบรารียูทิลิตีการแคชได้ใน frameworks/ml/nn/driver/cache ไดเรกทอรี ไดเรกทอรีย่อย nnCache มีโค้ดพื้นที่เก็บข้อมูลถาวรสำหรับไดรเวอร์เพื่อใช้ การแคชการคอมไพล์โดยไม่ต้องใช้ฟีเจอร์การแคช NNAPI การแคชการคอมไพล์รูปแบบนี้สามารถใช้ได้กับ NN HAL ทุกเวอร์ชัน หากไดรเวอร์เลือกที่จะใช้การแคชที่ไม่ได้เชื่อมต่อกับอินเทอร์เฟซ HAL ไดรเวอร์มีหน้าที่รับผิดชอบในการปล่อยอาร์ติแฟกต์ที่แคชไว้เมื่อไม่จำเป็นต้องใช้อีกต่อไป