Android 上的裝置端 AI:Delegate、NPU 與碎片化

Back
Category : News

Multigrid

在 Android 上實現裝置端 AI 的困難之處不在於撰寫推論呼叫,而是相同的呼叫必須在數千種系統單晶片與驅動程式組合上執行。其中有些能優雅地加速你的模型,有些會在不告知的情況下退回 CPU,還有少數會產生錯誤的數值。有效的策略是在執行階段探測並記住結果。



問題不在於 API,而在於差異性

在 iOS 上,你只需針對單一廠商的少數幾代晶片。而在 Android 上,你必須面對多家晶片廠商、各自多個世代,以及由裝置製造商按照自己的時程所出貨的驅動程式堆疊。兩支搭載相同旗艦晶片的手機,可能因為其中一支使用較舊的 GPU 驅動程式而表現不同。

由此導出的規則是:永遠不要依據裝置型號字串來決定是否加速。依裝置名稱建立的白名單會在一個版本週期內過期,無法涵蓋你出貨後才推出的裝置,而且它們是在有實際測量可用的情況下仍做出的猜測。應該在裝置上實際嘗試一次,並快取結果。



你所選擇的各層抽象

大致上有三種可用的抽象層級,選擇正確的層級主要取決於你需要多少控制權:

  • 由平台提供的受管理裝置端模型。 Google 提供系統層級的生成式 AI 功能,應用程式可以呼叫,模型由平台管理並在應用程式外部更新。這能讓你的 bundle 大小為零,但完全沒有控制權:可用性取決於裝置與系統元件版本,因此你的功能在缺少這些條件時必須降級。請以程式方式檢查目前可用性,而不是假設最低 API 等級。
  • 你自行打包的執行環境,搭配 delegate。 LiteRT(先前稱為 TensorFlow Lite)或 ONNX Runtime,在載入時選擇加速 delegate 或執行提供者。這是主流選擇。你可以控制模型、版本與後備機制。
  • 直接針對單一晶片家族的廠商 SDK。 晶片廠商會發布自己的神經網路 SDK,通常能從自家 NPU 榨取出比通用 delegate 更多的效能,但代價是需要額外的整合工作,並且每個廠商都要準備獨立的模型成品。當推論本身就是產品時值得這麼做;若推論只是其中一項功能則很少值得。

哪些加速路徑目前有效、哪些已被棄用而改用廠商 SDK,已改變過多次且仍持續變化。請將你選擇的 delegate 視為建置的參數,而非 Android 的既定事實,並在每次主要平台版本發布時重新檢查。以下的探測程式碼設計成只需修改一行就能更換 delegate。



能力探測器

無論你選擇哪種執行環境,模式都相同:嘗試建構加速的直譯器、執行固定的輸入、與 CPU 參考值比對,並計時兩者。如果建構拋出例外、數值差異超過容忍範圍,或加速路徑實際上並未更快,則使用 CPU。

enum Accel { NNAPI_OR_VENDOR, GPU, CPU }

data class ProbeResult(val accel: Accel, val medianMs: Double, val valid: Boolean)

fun probe(context: Context, modelBytes: ByteBuffer): Accel {
    val golden = loadGoldenInputOutput(context)   // fixed input + expected output
    val results = mutableListOf<ProbeResult>()

    // Always establish the CPU reference first: it is the tie-breaker
    // for both correctness and speed.
    val cpu = timeRun(modelBytes, Accel.CPU, golden)
    results += cpu

    for (accel in listOf(Accel.NNAPI_OR_VENDOR, Accel.GPU)) {
        val r = try {
            timeRun(modelBytes, accel, golden)
        } catch (t: Throwable) {
            // Delegate construction failing is normal, not exceptional.
            ProbeResult(accel, Double.MAX_VALUE, valid = false)
        }
        results += r
    }

    val best = results
        .filter { it.valid }
        .minByOrNull { it.medianMs } ?: cpu

    // Require a real margin. A 5% win is not worth a second code path.
    return if (best.medianMs < cpu.medianMs * 0.8) best.accel else Accel.CPU
}

Enter fullscreen mode

Exit fullscreen mode

在功能首次被使用時,於背景執行緒執行此探測一次——不要在應用程式啟動時執行,因為那時它會與其他所有工作競爭啟動預算。將判斷結果以你的應用程式版本、模型版本與 OS build number 為鍵值持久化儲存,並在這三者任一改變時重新探測。系統更新可能會新增或移除可用的驅動程式,而你的快取答案不能比它活得更久。



與黃金參考值進行驗證

探測中正確性這一半是最常被整合忽略的部分,卻也是能抓住最嚴重 bug 類型的那一部分:一個能執行、速度快、但結果錯誤的 delegate。

  1. 選擇一個能有效測試模型的固定輸入——不要用全零,因為許多有問題的 kernel 會意外地正確處理全零。
  2. 使用你信任的參考實作,在離線狀態下計算一次預期輸出,並以 asset 形式出貨。
  3. 在裝置上,以適合精確度的容忍值進行元素逐一比對。使用 fp16 進行計算 的 delegate 不會與 fp32 參考值完全相符,因此在正規化後的輸出上使用約 1e-2 的絕對容忍值通常是正確的;完全匹配測試會在每一台正常的裝置上失敗。
  4. 對於分類器,還要斷言 top-1 標籤是否相符。數值容忍可能會掩蓋類別翻轉,而類別翻轉才是使用者實際看到的結果。

使用 SoC 識別碼與 OS build 來記錄探測結果。幾週後,這些記錄會成為你自己安裝基礎唯一準確的地圖,其價值遠超過任何公開的相容性矩陣。



針對等級而非裝置進行設計

與其設計一個必須在所有地方都能運作的體驗,不如定義兩到三個等級,讓探測來為裝置指派等級:

等級 描述
accelerated 探測找到有效且明顯更快的路徑。完整功能:更大的模型、更長的上下文、即時處理。
cpu-capable 沒有加速,但 CPU 推論能在互動預算內完成。較小的模型、批次處理或依需求執行而非即時。
unsupported CPU 推論太慢,或記憶體不允許載入模型。功能被隱藏或改由遠端提供。隱藏它是誠實的做法;一個需要九秒的功能比沒有功能更糟糕。

透過實際測量來決定,意味著隨著你的安裝基礎變化,等級指派仍能保持正確,也意味著在你推出後才發布的新手機能自動獲得良好的體驗。這也為你提供了一個乾淨的地方來放置緊急停止開關:如果某個 delegate 在部分使用者群上被發現有問題,你可以透過遠端旗標將該群組降級到 CPU,而不是推出緊急版本。此模式與 模型與提示的機能旗標 中描述的模式相同。



你實際會遇到的失敗情況

  • 無聲的 CPU 退回。 Delegate 成功建構、未回報錯誤,卻因為某些運算子不支援而將大部分運算圖在 CPU 上執行。只有計時能揭露這一點。這也是為什麼探測要與 CPU 比較,而非僅檢查建構是否成功。
  • Delegate 初始化成本。 建構一個加速的直譯器可能需要數百毫秒甚至更久,因為驅動程式需要編譯運算圖。必須加以分攤:讓直譯器在整個 session 中保持存活,而非每次推論都重新建構,且永遠不要在主執行緒上建構。
  • 量化不匹配。 許多 NPU 只能執行整數模型。浮點模型會被 delegate 拒絕,或是無聲地在其他地方執行。如果你很在意 NPU 路徑,請出貨量化後的成品,並針對黃金參考值進行驗證——量化是一種數值變更,而且 哪些層能容忍它 並不一致。
  • 背景執行限制。 Android 不會讓你在背景應用程式中無限使用 NPU。長時間的工作應該放在前景服務並顯示通知,或是放在可延遲的背景工作佇列,由系統在裝置閒置且充電時排程執行。
  • 入門級裝置的記憶體壓力。 透過釋放直譯器並在需要時延遲載入來處理 onTrimMemory。在背景 session 中持續保留模型是導致應用程式被殺掉、然後被怪罪冷啟動變慢的常見原因。

如果 unsupported 等級退回至託管模型,則這兩條路徑在呼叫端必須可以互換——Multigrid 在各提供者之間暴露單一 API,並提供每次請求的成本與延遲,這讓遠端分支保持為單一實作,而非針對每個可能路由到的廠商各有一個。



相關文章

https://dev.to/multigrid/on-device-ai-on-android-delegates-npus-and-fragmentation-2o2p

https://www.worldprogramming.org/posts/on-device-ai-on-android-delegates-npus-and-fragmentation-ng8pca