☰
L3-InternLM 1.8B 模型 Android 端侧部署实践:用 TaoToken 统一 Key 打通模型转换与 mlc-llm 编译链路
2026/10/1 14:51:33 网站建设 项目流程

1. 为什么要在 Android 上跑 InternLM 1.8B:端侧推理的真实场景

InternLM 1.8B 是上海人工智能实验室开源的小参数对话模型,1.8B 的体量在量化到 q4f16_1 之后,权重文件大约 1GB 出头,推理时峰值内存落在 2GB 到 3GB 之间。这个数字意味着什么?意味着它刚好卡在中端 Android 机型的可用区间里——8GB 内存的手机跑起来不会把系统挤爆,而 4GB 内存的老机型大概率会在加载阶段就被系统杀掉进程。所以「InternLM 1.8B 模型 Android 端侧部署」这件事,本质上是在验证一条链路:模型转换能不能在开发机上顺利完成、mlc-llm 编译出来的 so 库能不能在 arm64 上正常加载、真机推理的首 token 延迟和内存占用到底是多少。

端侧部署和调云端 API 是两种完全不同的体验。云端 API 你关心的是网络往返和并发配额,端侧你关心的是内存峰值、线程调度、GPU 后端是否可用。InternLM 1.8B 适合谁?适合想验证「离线可用对话助手」这个产品形态的开发者,适合做隐私敏感场景(数据不出设备)的团队,也适合单纯想搞明白 mlc-llm 这套编译链路怎么走通的人。它不适合追求大模型能力上限的场景,1.8B 的知识广度和推理深度有限,但作为端侧链路的验证载体,它的体积和效果平衡得不错。

整条链路可以拆成三段:第一段在 x86 开发机上用 mlc_llm 的 convert_weight 把 HuggingFace 格式的权重转成 MLC 格式并量化;第二段用 mlc_llm compile 把计算图编译成目标平台的动态库,Android 上就是 arm64 的 so;第三段把模型和库打进 APK,或者通过 adb 推到设备存储里,让 App 加载。这三段里最容易卡住的是第二段的 NDK 工具链配置和第三段的签名与 gradle 配置,后面会逐个给可复制的命令。

调试期还有一个绕不开的问题:你在开发机上验证转换结果、对比不同量化参数的效果、或者临时调一下云端模型做对照,这些接口调用如果每个服务都单独配一套 Key,管理起来很碎。用 TaoToken 统一 Key 的好处是,转换和编译阶段用本地算力,验证和对照阶段走同一个 API 通道,Base URL 和 Key 只维护一份,切换模型只改 Model ID。这样整条链路的调试成本会低很多,尤其是你需要反复对比「端侧 q4f16_1 的输出」和「云端同系列模型的输出」时。

2. 前置准备:Rust、Android Studio 与 mlc-llm 环境搭建

这一节的目标是把开发机上的工具链装齐,让后面 convert_weight 和 compile 能跑起来。我试过在纯净的 Ubuntu 环境从零装一遍,踩过的坑主要集中在 Rust 镜像源和 NDK 路径上,下面按顺序来。

先装 Rust。国内网络直接拉 rustup 容易超时,换成中科大的镜像会稳很多。三条命令依次执行,安装脚本里出现选项直接回车用默认值:

export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup curl --proto '=https' --tlsv1.2 -sSf https://mirrors.ustc.edu.cn/misc/rustup-install.sh | sh

装完之后记得 source 一下环境,否则后面 mlc_llm 编译时找不到 cargo:

. "$HOME/.cargo/env" rustc --version

接着装 Android Studio 和命令行工具。这里不需要真的打开图形界面,我们只用它的 SDK、NDK 和 JBR(JetBrains Runtime,自带 JDK)。下载解压到 /root/android 下:

mkdir -p /root/android && cd /root/android wget https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2024.1.1.12/android-studio-2024.1.1.12-linux.tar.gz tar -xvzf android-studio-2024.1.1.12-linux.tar.gz cd android-studio wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip?hl=zh-cn unzip commandlinetools-linux-11076708_latest.zip\?hl\=zh-cn

然后用 sdkmanager 装 NDK、CMake 和对应的 platform。NDK 版本建议锁 27.0.12077973,这个版本和 mlc-llm 的编译脚本兼容性最好:

export JAVA_HOME=/root/android/android-studio/jbr cmdline-tools/bin/sdkmanager "ndk;27.0.12077973" "cmake;3.22.1" "platforms;android-34" "build-tools;33.0.1" --sdk_root='sdk'

环境变量是这一节的关键,写错一个后面编译就报找不到编译器。把下面这段加到 ~/.bashrc 里,然后 source 生效:

export ANDROID_NDK=/root/android/android-studio/sdk/ndk/27.0.12077973 export TVM_NDK_CC=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang export JAVA_HOME=/root/android/android-studio/jbr export ANDROID_HOME=/root/android/android-studio/sdk export PATH=/root/android/android-studio/sdk/cmake/3.22.1/bin:$PATH

TVM_NDK_CC 这个变量指向的是 aarch64 的 clang 交叉编译器,mlc-llm 编译 Android 库时会读它。如果这个路径写错,编译阶段会报aarch64-linux-android24-clang: not found,这是最常见的报错之一。

然后是 mlc-llm 本体。建议用 conda 建独立环境,Python 版本锁 3.11:

conda create --name mlc-prebuilt python=3.11 conda activate mlc-prebuilt conda install -c conda-forge git-lfs

mlc-llm 的 nightly wheel 从官方源装可能慢,可以先下载到本地再 pip install。注意 wheel 链接会随版本更新变化,装之前去 mlc.ai/wheels 确认当前版本号:

wget https://github.com/mlc-ai/package/releases/download/v0.9.dev0/mlc_llm_nightly_cu122-0.1.dev1519-cp311-cp311-manylinux_2_28_x86_64.whl wget https://github.com/mlc-ai/package/releases/download/v0.9.dev0/mlc_ai_nightly_cu122-0.15.dev559-cp311-cp311-manylinux_2_28_x86_64.whl pip install mlc_llm_nightly_cu122-0.1.dev1519-cp311-cp311-manylinux_2_28_x86_64.whl pip install mlc_ai_nightly_cu122-0.15.dev559-cp311-cp311-manylinux_2_28_x86_64.whl

验证安装是否成功,能打印出模块路径就说明没问题:

python -c "import mlc_llm; print(mlc_llm)"

最后克隆 mlc-llm 仓库,注意要拉子模块,TVM 在 3rdparty 目录下:

git clone https://github.com/mlc-ai/mlc-llm.git cd mlc-llm git submodule update --init --recursive

到这里环境就齐了。这一步的检查清单:rustc 能输出版本、ANDROID_NDK 路径存在、TVM_NDK_CC 指向的 clang 可执行、mlc_llm 能 import、mlc-llm 仓库子模块拉全。五个都过,再往下走。

3. 模型转换与 mlc-llm 编译:可复制的配置与命令

这一节是整条链路的核心,分两步:convert_weight 把原始权重转成 MLC 格式并量化,gen_config 生成运行时配置。先准备模型目录,把 InternLM 1.8B 的原始权重放好:

mkdir -p /root/models/ ln -s /share/new_models/Shanghai_AI_Laboratory/internlm2_5-1_8b-chat /root/models/internlm2_5-1_8b-chat

转换命令的关键参数是--quantization q4f16_1,这个量化方案把权重量化到 4bit、激活保持 fp16,在端侧是精度和体积的平衡点。输出目录用-o指定:

cd android/MLCChat export TVM_SOURCE_DIR=/root/android/mlc-llm/3rdparty/tvm export MLC_LLM_SOURCE_DIR=/root/android/mlc-llm mlc_llm convert_weight /root/models/internlm2_5-1_8b-chat/ \ --quantization q4f16_1 \ -o dist/internlm2_5-1_8b-chat-q4f16_1-MLC

转换完成后生成配置。conv-template 用 chatml,因为 InternLM 2.5 的对话模板就是 chatml 格式,写错会导致模型输出乱掉:

mlc_llm gen_config /root/models/internlm2_5-1_8b-chat/ \ --quantization q4f16_1 --conv-template chatml \ -o dist/internlm2_5-1_8b-chat-q4f16_1-MLC

gen_config 这一步容易报缺包,把这三个装上基本就解决:

pip install transformers sentencepiece protobuf

生成的 mlc-chat-config.json 是运行时读的配置文件,里面记录了模型架构、量化方式、上下文长度、对话模板等。你可以打开看一眼,确认model_type是 internlm2、quantization是 q4f16_1。这个文件后面打包时会一起进 APK。

打包前先本地验证转换结果,把计算图编译成 CUDA 库在开发机上跑一遍。这一步能提前暴露模型转换的问题,避免推到手机上才发现:

mkdir -p dist/libs mlc_llm compile ./dist/internlm2_5-1_8b-chat-q4f16_1-MLC/mlc-chat-config.json \ --device cuda -o dist/libs/internlm2_5-1_8b-chat-q4f16_1-MLC-cuda.so

然后用 MLCEngine 加载跑一次对话,确认输出正常:

from mlc_llm import MLCEngine engine = MLCEngine( model="./dist/internlm2_5-1_8b-chat-q4f16_1-MLC", model_lib="./dist/libs/internlm2_5-1_8b-chat-q4f16_1-MLC-cuda.so" ) print(engine) for response in engine.chat.completions.create( messages=[{"role": "user", "content": "你是谁?"}], stream=True ): for choice in response.choices: print(choice.delta.content, end="", flush=True) print("\n") engine.terminate()

开发机验证通过后,改打包配置。mlc-package-config.json 里列出要打进 APK 的模型,model 字段用 HF:// 前缀指向 HuggingFace 仓库,model_id 是 App 里显示的标识:

{ "device": "android", "model_list": [ { "model": "HF://timws/internlm2_5-1_8b-chat-q4f16_1-MLC", "estimated_vram_bytes": 3980990464, "model_id": "internlm2_5-1_8b-chat-q4f16_1-MLC" } ] }

这里有个路径坑要注意:打包命令必须在 MLCChat 目录下执行,如果你之前把 mlc-llm 克隆到了 android-studio 里面,先移出来:

cd /root/android/android-studio mv mlc-llm /root/android cd /root/android/mlc-llm/android/MLCChat mlc_llm package

mlc_llm package会读取上面的 json,把模型权重和编译好的库一起打进 App 的 assets。这一步需要能访问 HuggingFace 拉取模型元数据,如果网络不通会卡在下载阶段。

调试期如果你需要对照云端模型的输出,比如想看看同一个 prompt 在更大参数模型上的表现,可以用 TaoToken 的 API 通道。Base URL 填https://taotoken.net/api,Key 在控制台生成,Model ID 按需选。这样端侧和云端的对照实验在同一个 Key 下管理,不用来回切配置。生成 Key 的入口在 https://taotoken.net/api-keys ,接入文档在 https://taotoken.net/doc 。

4. 真机验证:签名、gradle 配置与 adb 安装

打包出来的 APK 要装到手机上,签名和 gradle 配置是绕不过去的。服务器上构建没有 Android Studio 的图形界面,必须用命令行 keytool 生成签名文件:

cd /root/android/mlc-llm/android/MLCChat /root/android/android-studio/jbr/bin/keytool -genkey -v \ -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000

交互式问答里,密码自己设一个记住,姓名和组织随便填,国家代码填 CN。生成后得到 my-release-key.jks。

然后改 app/build.gradle,核心是加 signingConfigs 段并把 release 构建指向它。注意 storeFile 的路径要写绝对路径,密码和 alias 和你 keytool 时填的一致:

signingConfigs { release { storeFile file("/root/android/mlc-llm/android/MLCChat/my-release-key.jks") storePassword "123456" keyAlias "mykey" keyPassword "123456" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } }

同时把 defaultConfig 里原来的 signingConfig 引用删掉,避免冲突。改完执行编译:

./gradlew assembleRelease

编译成功后 APK 在app/build/outputs/apk/release/app-release.apk。用 adb 装到手机上:

adb install -r app/build/outputs/apk/release/app-release.apk

装完打开 App,首次启动会从 HuggingFace 下载模型权重,大约 1GB 多,需要设备能访问外网。如果不想让 App 联网下载,可以用 bundle 方式把模型数据通过 adb 推到设备存储,具体路径参考 mlc-llm 文档里的 bundle 说明。

真机跑起来之后,重点记录两个指标:首 token 延迟和内存占用。首 token 延迟可以用 App 内的日志或者 adb logcat 抓,内存占用用adb shell dumpsys meminfo ai.mlc.mlcchat看 PSS 值。中端机型(骁龙 7 系、8GB 内存)上,q4f16_1 量化的 InternLM 1.8B 首 token 延迟大概在几百毫秒到 1 秒多,取决于 CPU 调度和是否用上了 GPU 后端。内存峰值落在 2GB 到 3GB 之间,如果超过 3.5GB 就要考虑换更激进的量化或者缩短上下文。

运行闪退的话,先看 logcat 里有没有UnsatisfiedLinkError,这通常是 so 库架构不对或者没打进 APK;如果是加载模型时被杀,看dumpsys meminfo的峰值,多半是内存不够。模型下载不完整也会闪退,删掉 App 数据重新下载一次。

5. 常见报错排查:从 401 到 local proxy failed

这一节把链路上高频出现的报错列出来,对照着查能省不少时间。

报错一:aarch64-linux-android24-clang: not found

这是 TVM_NDK_CC 没设或者路径写错。检查echo $TVM_NDK_CC输出的路径下有没有这个可执行文件,没有的话确认 NDK 版本是不是 27.0.12077973,路径里的linux-x86_64和aarch64-linux-android24-clang拼写要对。

报错二:ModuleNotFoundError: No module named 'transformers'

gen_config 阶段缺包,pip install transformers sentencepiece protobuf装上即可。如果装了还报,确认当前 conda 环境是 mlc-prebuilt。

报错三:401 Unauthorized

这个在调云端 API 做对照实验时出现,说明 Key 没传或者传错。检查请求头里的 Authorization 字段格式是不是Bearer <你的Key>,Key 有没有多余空格。TaoToken 的 Key 在控制台生成,如果 Key 被删了或者过期,重新生成一个。Base URL 确认是https://taotoken.net/api,不要带多余的路径后缀。

报错四:local proxy failed或连接超时

这个通常出现在 mlc_llm package 拉 HuggingFace 模型元数据时。检查开发机的网络能不能访问 HuggingFace,如果不行,可以先把模型权重下载到本地,用本地路径替代 HF:// 前缀。注意这里说的是正常的网络连通性问题,不涉及任何特殊网络工具。

报错五:Error reading choices或流式输出解析失败

这个在 Python 脚本里调云端 API 时出现,多半是 stream 模式下响应格式和解析代码不匹配。检查response.choices的结构,有些兼容层返回的是response.choices[0].delta.content,有些是response.choices[0].message.content。打印一下原始 response 看结构。

报错六:OAuth相关错误

如果用的是需要 OAuth 的接入方式,token 过期会报这个。重新走一遍授权流程拿新 token。用 API Key 方式的话不会遇到这个问题。

报错七:gradle 编译报签名冲突

SigningConfig "release" is missing required property或者Keystore was tampered with。前者是 build.gradle 里 signingConfigs 段没配对,后者是密码填错。确认 storePassword 和 keyPassword 和 keytool 时设的一致,storeFile 路径是绝对路径且文件存在。

报错八:App 启动后白屏或立即退出

先adb logcat | grep mlc看有没有 native 层报错。如果是dlopen failed,说明 so 库没打进 APK 或者 ABI 不匹配。检查 app/build.gradle 里的 ndk abiFilters 是否包含 arm64-v8a,以及 mlc4j 模块有没有正确编译。

排查的顺序建议是:先确认环境变量,再确认模型转换产物完整,然后确认 APK 里的 so 和模型文件,最后看运行时日志。大部分问题在前两步就能定位。

6. 把 Key 管理收拢:端侧调试期的接口调用实践

端侧部署的调试期,你其实同时在跟两套东西打交道:本地编译出来的模型库,和云端用来做对照的 API。本地这套不用管 Key,但云端这套如果每个服务单独配,切换模型、对比输出、验证 prompt 效果的时候会很碎。用 TaoToken 统一 Key 的价值就在这里——Base URL 固定https://taotoken.net/api,Key 一份,Model ID 按需换。

具体怎么用?比如你想对比端侧 q4f16_1 量化的 InternLM 1.8B 和云端更大参数模型的输出差异,写个脚本同时调两边,端侧走本地 MLCEngine,云端走 OpenAI 兼容接口:

from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key="你的TaoToken Key" ) resp = client.chat.completions.create( model="internlm2_5-1_8b-chat", messages=[{"role": "user", "content": "用一句话解释端侧推理"}], stream=False ) print(resp.choices[0].message.content)

这样端侧和云端的对照实验在同一个脚本里完成,Key 只维护一份。如果你在做长期的编码类 Agent 调试,或者需要反复跑大量 prompt 做效果对比,Coding Plan 会比按量调用更划算,入口在 https://taotoken.net/coding-plan 。模型对话的在线验证入口在 https://taotoken.net/models ,接入文档在 https://taotoken.net/doc ,API Key 管理在 https://taotoken.net/api-keys 。

回到端侧本身,跑通之后的优化方向有几个:一是换量化方案,q4f16_1 之外还有 q4f32_1 和 q3f16_1,精度和体积的取舍不同;二是调线程数,mlc-llm 的运行时可以配 num_threads,中端机型上 4 线程通常比 8 线程更稳,因为大核数量有限;三是缩短上下文长度,mlc-chat-config.json 里的 context_window_size 调小能显著降内存。这几个参数调完,首 token 延迟和内存占用会有明显变化,建议每次只改一个变量,记录数据对比。

真机验证的时候,adb shell dumpsys meminfo ai.mlc.mlcchat | grep TOTAL能直接看到 PSS 总量,配合 logcat 里的首 token 时间戳,两个指标一起记。中端机型上如果首 token 超过 2 秒,优先检查是不是没用上 GPU 后端,mlc-llm 在 Android 上支持 OpenCL,配置对了能快不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询