
边缘 AI 架构设计:在手机和 IoT 上运行 LLM
为什么要把 LLM 放到边缘设备上 2026 年,端侧运行 LLM 不再是学术论文里的概念,而是产品需求。原因很直接: 延迟:云上 LLM 往返延迟 200-800ms,端侧可以做到 20-50ms 隐私:医疗、金融场景的数据不能离开设备 离线:网络不稳定场景(工业、户外)需要离线推理 成本:云端推理 $0.01/千 token,端侧摊到设备折旧里几乎免费 但挑战也真实:手机内存 8-16GB,要跑 7B 模型需要 14GB(FP16),得压缩。 模型压缩三板斧 量化:用更少的 bit 表示权重 量化方案 位宽 7B 模型大小 精度损失 硬件支持 FP16 (基线) 16-bit 13.5 GB 0% 全部 INT8 8-bit 6.8 GB <1% CPU/GPU/NPU INT4 (GPTQ) 4-bit 3.5 GB 2-3% GPU/NPU INT4 (AWQ) 4-bit 3.5 GB 1-2% GPU/NPU INT3 3-bit 2.6 GB 5-8% 部分 NPU 1.58-bit ~2-bit 1.8 GB 8-12% 实验阶段 # 使用 llama.cpp 的量化工具链 # 1. 转换为 GGUF 格式 # python convert.py models/llama-3-8b --outtype f16 --outfile llama-3-8b-f16.gguf # 2. 量化为 INT4 # ./quantize llama-3-8b-f16.gguf llama-3-8b-q4_k_m.gguf Q4_K_M # 在 Python 中使用 llama-cpp-python 加载 from llama_cpp import Llama llm = Llama( model_path="llama-3-8b-q4_k_m.gguf", n_ctx=2048, # 上下文窗口(移动端建议 2048-4096) n_threads=4, # CPU 线程数 n_gpu_layers=0, # 移动端通常 0(纯 CPU) n_batch=512, # 批处理大小 mmap=True, # 内存映射加载,减少内存占用 mmap_timeout=30000, ) response = llm( "解释量子计算的基本原理", max_tokens=256, temperature=0.7, top_p=0.9, ) 蒸馏:小模型学大模型 # 使用 Hugging Face 进行知识蒸馏 from transformers import AutoModelForCausalLM, AutoTokenizer import torch import torch.nn.functional as F teacher = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-70B") student = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-1B") teacher.eval() student.train() optimizer = torch.optim.AdamW(student.parameters(), lr=5e-5) for batch in dataloader: with torch.no_grad(): teacher_logits = teacher(batch["input_ids"]).logits teacher_probs = F.softmax(teacher_logits / 2.0, dim=-1) # 温度=2 student_logits = student(batch["input_ids"]).logits student_log_probs = F.log_softmax(student_logits / 2.0, dim=-1) # KL 散度损失 loss = F.kl_div(student_log_probs, teacher_probs, reduction="batchmean") # 加上 hard label loss loss += 0.3 * F.cross_entropy( student_logits.view(-1, student_logits.size(-1)), batch["labels"].view(-1) ) loss.backward() optimizer.step() optimizer.zero_grad() 剪枝:去掉不重要的权重 # 结构化剪枝:移除整个注意力头或层 import torch.nn.utils.prune as prune def structured_prune(model, amount=0.3): """移除 30% 最不重要的注意力头""" for name, module in model.named_modules(): if isinstance(module, torch.nn.Linear): # 按 L1 范数剪枝 prune.l1_unstructured(module, name="weight", amount=amount) # 固化剪枝 prune.remove(module, "weight") return model # 评估剪枝后的精度 def evaluate_pruned(model, eval_dataset): pruned = structured_prune(model, amount=0.3) # 重新微调恢复精度 trainer = Trainer( model=pruned, args=TrainingArguments( output_dir="./pruned-finetune", num_train_epochs=2, learning_rate=2e-5, ), train_dataset=train_dataset, eval_dataset=eval_dataset, ) trainer.train() return pruned 端侧推理引擎对比 llama.cpp Mobile // Android NDK 集成示例 // JNI 接口:从 Java/Kotlin 调用 llama.cpp #include "llama.h" #include <jni.h> #include <android/log.h> extern "C" JNIEXPORT jlong JNICALL Java_com_example_llm_LlamaEngine_nativeInit(JNIEnv* env, jobject thiz, jstring model_path) { const char* path = env->GetStringUTFChars(model_path, nullptr); llama_backend_init(false); llama_model_params model_params = llama_model_default_params(); model_params.n_gpu_layers = 0; // 移动端纯 CPU model_params.use_mmap = true; llama_model* model = llama_load_model_from_file(path, model_params); env->ReleaseStringUTFChars(model_path, path); return (jlong)model; } extern "C" JNIEXPORT jstring JNICALL Java_com_example_llm_LlamaEngine_nativeGenerate( JNIEnv* env, jobject thiz, jlong model_ptr, jstring prompt, jint max_tokens ) { llama_model* model = (llama_model*)model_ptr; const char* prompt_str = env->GetStringUTFChars(prompt, nullptr); llama_context_params ctx_params = llama_context_default_params(); ctx_params.n_ctx = 2048; ctx_params.n_threads = 4; ctx_params.n_batch = 512; llama_context* ctx = llama_new_context_with_model(model, ctx_params); // Tokenize std::string text(prompt_str); std::vector<llama_token> tokens = llama_tokenize(ctx, text, true); // 生成循环 std::string result; for (int i = 0; i < max_tokens; i++) { if (llama_decode(ctx, llama_batch_get_one(&tokens.back(), 1, i, 0))) break; llama_token new_token = llama_sample_token(ctx, nullptr, 0.8f, 0.9f, 32); if (new_token == llama_token_eos(ctx)) break; tokens.push_back(new_token); result += llama_token_to_piece(ctx, new_token); } llama_free(ctx); env->ReleaseStringUTFChars(prompt, prompt_str); return env->NewStringUTF(result.c_str()); } MLC-LLM # MLC-LLM:使用 TVM 编译器优化,支持 Android/iOS # 编译模型到移动端 """ # 1. 编译模型 python -m mlc_llm compile \ --model llama-3-8b \ --quantization q4f16_1 \ --target android \ --output llama-3-8b-q4f16_1-android.tar # 2. 在 Android 上加载 """ # Swift (iOS) 示例 """ import MLCEngine let engine = MLCEngine() try await engine.load( model: "Llama-3-8B-q4f16_1", modelPath: Bundle.main.path(forResource: "llama-3-8b", ofType: "mlc")! ) let response = try await engine.generate( prompt: "写一首关于秋天的诗", maxTokens: 256, temperature: 0.7 ) """ 引擎性能对比(Llama-3-8B INT4) 引擎 平台 首 token 延迟 吞吐 (tok/s) 内存峰值 包大小 llama.cpp Android 800ms 12-18 4.2 GB 3.5 GB MLC-LLM Android 500ms 18-25 4.0 GB 3.6 GB MLC-LLM iOS (Metal) 200ms 30-40 3.8 GB 3.5 GB ExecuTorch Android 600ms 15-22 4.1 GB 3.5 GB ONNX Runtime Android 900ms 10-15 4.5 GB 4.0 GB NPU 加速 现代手机都有 NPU(神经网络处理器),但 LLM 在 NPU 上的支持还很不成熟。 ...
