標籤: iOS 端側推理

  • 270 億參數上 iPhone 的實戰路線

    270 億參數上 iPhone 的實戰路線

    📌 本文重點

    • 27B 大模型壓縮後可在手機端側實用
    • 量化 +剪枝 +蒸餾是端側部署核心組合
    • 善用 NPU/GPU 與 RAG/Agent 架構提升體驗

    在端側塞進 Qwen 3.6-27B 這種體量的模型,直接解決了三個痛點

    1. 隱私 & 合規:聊天、RAG 查文件、簡單 Agent 全在本機,不丟到雲端。
    2. 互動延遲:減少網路 RTT,短指令(改句子、補程式)能接近即時回應。
    3. 成本 & 擴展:不用為每個活躍使用者準備一個雲端 GPU,端側模型成為主力推理,引流雲端模型解決長上下文或高難任務。

    PrismML 把 Qwen 3.6-27B 壓到能在 iPhone 17 Pro 上跑,給了我們一個清楚訊號:27B 級模型上手機是可行路線,但你要整套壓縮 + runtime + 系統調優一起做


    重點說明:端側大模型的幾個關鍵技術

    1. 量化:從 16bit 壓到 4bit / 2bit

    目的:壓縮權重體積 + 減輕記憶體頻寬壓力

    • 粗估:
    • FP16:27B × 2 bytes ≈ 54 GB(純權重就爆)
    • 4bit:27B × 0.5 bytes ≈ 13.5 GB
    • 2bit:27B × 0.25 bytes ≈ 6.75 GB

    💡 關鍵: 把 27B 權重從 54GB 壓到約 7–14GB,是端側部署能否落地的基礎門檻

    • 實務上常用 mixed-precision
    • attention / embedding 保留 8bit 或 16bit
    • MLP 可用 4bit 或 2bit(配合 group-wise scaling)
    • 在 iOS 端常見兩種路線:
    • GGUF + llama.cpp:用 Q4_K_M / Q3_K_M 這類 profile
    • Core ML / MLC:用 symmetric int4 / int8,用 LUT 把量化常數 bake 進權重

    對專案的好處:

    • 同一隻 app 可以提供 小模型 + 壓縮大模型 的雙模式:日常用 7B,開「專業模式」時啟動 27B(但速度變慢 + 發熱)。

    2. 剪枝與低秩分解:不只變小,還要能跑快

    量化只壓體積,不一定壓算力;要在功耗有限的手機跑得動,需要再用:

    1. 結構化剪枝(structured pruning)
    2. 砍掉整個 head、channel 或 FFN neuron。
    3. 例如把 32 heads 剪到 24 heads,或把 FFN hidden dim 從 8k 降到 6k。
    4. 這會直接減少 MACs,對 NPU / GPU 友善。
    5. 低秩分解(LoRA / SVD 分解 FFN 權重)
    6. 把大矩陣 (W \in \mathbb{R}^{d \times d}) 分成 (U \in \mathbb{R}^{d \times r}, V \in \mathbb{R}^{r \times d}),r ≪ d
    7. 搭配蒸餾可以維持合理能力。

    PrismML 類型的方案多半是 多階段:量化 + 剪枝 + 蒸餾 + runtime 特化 一起做,才有可能把 27B 折成 iPhone 等級的 latency。

    3. KV Cache / 推理圖優化:token/s 的決勝點

    端側 LLM 的痛點不是 model load,而是 持續推理的 token/s 與發熱

    關鍵是:

    • KV Cache 緊縮
    • 精度壓到 8bit/4bit(與權重量化分開考慮)。
    • 善用 sliding window / attention sink,減少長對話時的 KV 佔用。
    • 推理圖優化(graph optimization)
    • 透過 MLC / Core ML / 自研 runtime 把整段 transformer block fuse 成 1–2 個大 kernel。
    • 減少 CPU ↔ NPU ↔ GPU 之間的 context switch。

    效果:在 iPhone 17 Pro 這種等級裝置,27B 壓縮到 4bit + 優化 graph,合理預期 5–10 tok/s 左右(看溫控),可以應付聊天、簡單 RAG,但不是寫 3k 行程式碼那種長輸出場景。

    💡 關鍵: 端側大模型的體感速度主要由 token/s 決定,5–10 tok/s 大致只適合短回覆與簡單任務

    4. 善用手機 NPU / GPU:不是「丟上去就快」

    iOS 上大致有三條路:

    1. Core ML + ANE(NPU)
    2. 最省電,但要配合 Core ML 支援的 op / dtype,需要前置轉換。
    3. Metal / MLC
    4. 走 GPU / CPU + 少量 NPU,對自訂 kernel 比較自由。
    5. llama.cpp(Metal 後端)
    6. 直接吃 GGUF,開 Metal 加速;部分 op 仍在 CPU。

    實務上可以採用 混合策略:embedding / 前幾層在 NPU,後面在 GPU / CPU,平衡記憶體與溫度。


    實作範例:在 iOS 上做聊天 / RAG / Agent 的架構

    下面給一個「實際可落地」的 27B 壓縮部署思路,用 pseudo code 表示。

    1. 推理框架選型與模型準備

    方案 A:llama.cpp + GGUF(開發週期短)

    1. 先把 Qwen 3.6-27B 轉成 GGUF 並量化(在桌機完成):
    python convert_qwen_to_gguf.py \
      --model qwen-3.6-27b \
      --out qwen-3.6-27b-q4_k_m.gguf
    
    ./quantize \
      qwen-3.6-27b-q4_k_m.gguf \
      qwen-3.6-27b-q4_k_m.gguf \
      Q4_K_M
    
    1. iOS 使用 llama.cpp Swift / C API:
    let params = gpt_params_default()
    params.n_ctx = 4096
    params.n_gpu_layers = 20   // 前 20 層走 Metal
    
    let ctx = llama_init_from_file("qwen-3.6-27b-q4_k_m.gguf", params)
    
    func infer(prompt: String) -> String {
        llama_reset(ctx)
        llama_eval_prompt(ctx, prompt)
        var output = ""
        while !shouldStop(output) {
            let token = llama_eval_next(ctx)
            output += tokenizer.decode(token)
        }
        return output
    }
    

    注意:n_gpu_layers 設太高,會爆 VRAM / 發熱;設太低,會被 CPU 拖死。實測需要在 8–24 之間找平衡。

    方案 B:MLC LLM + Metal(更深度優化)

    • 透過 MLC 工具鏈把 Qwen 3.6-27B 壓成 int4 + fused kernels,生成 iOS 專用模型包:
    mlc_llm convert qwen-3.6-27b \
      --quantization q4f16_0 \
      --target iphone
    
    mlc_llm build ios qwen-3.6-27b-q4f16_0
    

    iOS 端呼叫:

    let engine = MLCChatEngine(model: "qwen-27b-q4f16_0")
    
    engine.generate(
      prompt: userPrompt,
      maxTokens: 512,
      temperature: 0.7,
      streamHandler: { token in
        appendToUI(token)
      }
    )
    

    實務建議:若不是要深度客製 runtime,MLC 會比自己改 llama.cpp 更好拿穩定 fps 和電源管理

    2. RAG:本機嵌入 + 文件切片

    架構:

    • 輕量 embedding 模型(如 384–768 dim)放端側
    • 文本切 chunk(512–1024 tokens),建立本地向量索引。
    • 查詢時在端側 embed + ANN 搜索 + top-k context 拼回 prompt,再丟給 27B 生成。

    簡易 Swift 流程(以 Core ML embedding 模型為例):

    // 1. 建立索引(啟動或背景時)
    for chunk in documentChunks {
        let emb = embeddingModel.encode(text: chunk.text) // [Float]
        annIndex.add(vector: emb, id: chunk.id)
    }
    
    // 2. 查詢時
    func ragAnswer(query: String) -> String {
        let qEmb = embeddingModel.encode(text: query)
        let topK = annIndex.search(query: qEmb, k: 4)
        let context = topK.map { $0.text }.joined(separator: "\
    ---\
    ")
    
        let prompt = """
    你是一個助理。以下是知識庫內容:
    \(context)
    
    問題:\(query)
    請根據知識庫回答,若沒有明確答案就說不知道。
    """
        return infer(prompt: prompt)
    }
    

    好處

    • 敏感文件不離開手機;
    • 即使 27B 很慢,RAG 因為 context 精準,需要生成的 token 也變少

    3. 簡單 Agent:有限循環 + 工具調用

    在端側做 Agent 不要太貪心,建議:

    • 限制 max steps(例如 4–6 步)。
    • 工具呼叫只做本機能力:檔案讀寫、日曆、藍牙裝置等。

    簡化 pseudo code:

    struct ToolCall { let name: String; let args: [String: Any] }
    
    func runAgent(task: String) {
        var state = ""
        for step in 0..<6 {
            let prompt = buildAgentPrompt(task: task, state: state)
            let output = infer(prompt: prompt)
    
            if let tool = parseToolCall(output) {
                let result = executeTool(tool)
                state += "\
    [tool-result]\
    " + result
            } else {
                renderFinal(output)
                break
            }
        }
    }
    

    這種架構在 27B 上 iPhone 實務可行,但要注意:token/s 慢 → 每一輪推理越短越好,prompt 盡量模板化、少廢話。


    建議與注意事項:坑在哪、怎麼踩得比較輕

    1. 記憶體與冷啟動

    • iPhone 17 Pro 類級別裝置,27B 4bit 純權重就十幾 GB,實務上需:
    • 模型分片 / 分層載入
      • 常駐 7B 小模型;
      • 27B 大模型在「高需求模式」才 mmap 載入部分層(如後 12 層)。
    • 或採 Mixture-of-Experts(MoE) 類方案,如 GLM 5.2 + Flash MoE,那類模型在 Mac M5 上可跑 2–2.8 tok/s,給端側設計方向參考:不是所有 layer 都要 full dense
    • 冷啟動:
    • 首次 load 27B 量化模型,可能 3–10 秒,一定要做 loading UI + 延遲初始化(app 啟動時先載小模型,使用者開「專業模式」才載大模型)。

    💡 關鍵: 3–10 秒的冷啟動延遲是端側大模型的 UX 關鍵,必須用分層載入與明確 UI 掩護

    2. 電量與發熱

    • 連續推理 1–2 分鐘,大模型會把 SoC 拉到 TDP 上限,掉頻後 token/s 會明顯下降。
    • 實作上建議:
    • 限制 max_tokens,避免長篇輸出。
    • 使用 溫度監控(ThermalState,高溫時降速:
    if ProcessInfo.processInfo.thermalState == .serious {
        params.n_threads = max(2, params.n_threads / 2)
    }
    

    3. 隱私與合規

    • Ce 合規角度,端側推理 + 本地 RAG 是加分項,但要注意:
    • 日誌、錯誤回報不得包含原文檔內容。
    • 若有雲端 fallback,要明確 UI 提示「此問題將送出至伺服器」。

    4. 測試方法與性能預估

    建議自建簡單基準腳本,統計:

    • 冷啟動時間(model load 完到第一 token)
    • token/s(前 50 tokens、整段平均)
    • 電池掉電率與機身溫度(5 分鐘連續推理)

    例:用 llama.cpp CLI 在開發機 / TestFlight 內部版測一次:

    ./main -m qwen-27b-q4_k_m.gguf \
      -p "Hello" -n 256 -s 42 --timings
    

    官方輸出會給:

    • prompt eval time / token
    • generation eval time / token

    再用這些數據估算:

    • 聊天:平均 50–100 tokens → 是否能在 3–5 秒內完成?
    • RAG:查詢 + 150 tokens → 整體延遲能否低於 10 秒?

    結論:端側 27B 的實際策略

    對多數 iOS 專案,建議的 可實作路線 是:

    1. 產品層:
    2. 7B–8B 模型 作為主力。
    3. 提供「離線高精度模式」載入壓縮 27B,給重度用戶 & 高價方案。
    4. 技術層:
    5. 量化採 4bit mixed-precision,配合剪枝 + 蒸餾。
    6. 選擇 MLC / llama.cpp Metal 後端,不要自幹 runtime,除非是公司核心技術。
    7. 系統層:
    8. 做好 分層部署 + 延遲載入 + 熱管理
    9. 嵌入模型 + RAG 索引用小模型處理,27B 只負責最終生成。

    這樣設計,你可以合理地把「看起來誇張的 27B」塞進 iPhone 裡,並且在聊天、RAG、輕量 Agent 這幾個場景拿到實際可用的體驗。真正的難點不只在模型壓縮,而是在整條端側推理鏈的工程化

    🚀 你現在可以做的事

    • 在開發機上用 llama.cpp 或 MLC 先跑一版 Qwen 3.6-27B 4bit,量測 token/s 與冷啟動時間
    • 選定一個 7B 模型做端側 RAG PoC,實作本地嵌入與向量索引流程
    • 規劃產品中的「專業模式」或「離線高精度模式」,設計 7B / 27B 雙模型切換與載入策略