ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DeepSeek端侧部署实战:NPU量化与推理优化指南

DeepSeek端侧部署实战:NPU量化与推理优化指南

1. 移动端部署 DeepSeek 前,先过一顿“算账”

这两个月我一直在折腾端侧大模型,手上的项目从语音摘要、写作助手到端侧知识库问答都试了一轮。最直观的感受是:模型面世的速度远远跑在了硬件适配前面。去年不少团队还在用 10B 以上的大模型做云端推理,今年开源社区里已经到处都是 1.5B、3B、7B 的蒸馏小模型,甚至还有人在 4G 内存的旧手机上跑通了对话。于是问题就变成一个非常现实的问题:这类低参数量模型,能不能可靠地吃下移动端那颗 NPU 的性能,做到平时不发热、断网也能用、随手打开就能说话。

先说结论:能,但别把 NPU 当成“免费的算力黑箱”。DeepSeek 系列尤其特殊——它是典型的 Transformer 大模型结构,在浮点算力、显存带宽、动态形状三个方面都刚好踩在移动端的痛点上。把它搬到手机 NPU 上,真正卡壳的往往不是模型的“肚子”有多大,而是四个字:喂不喂得饱。

1.1 内存墙、带宽墙与功耗墙

很多教程会告诉你“量化到 INT4 就好啦”,但实际操作中每个阶段的瓶颈完全不同。我把它们拆成三堵墙来理解:

  • 容量墙:模型权重多大,内存就得预留多大。以 7B 模型为例,FP16 权重要占 14GB,INT8 也要 7GB,INT4 才能压到 3.5GB 左右。手机内存一般 8GB 起步,但系统、App、NPU 驱动、推理框架还要瓜分一部分。结论很简单:7B 基本是移动端的上限,5B、3B、1.5B 才是日常。
  • 带宽墙:LLM 解码是一个“权重搬运”任务。每生成一个 token,就要把整套权重从内存搬到计算单元过一遍。手机上 LPDDR 内存带宽也就几十 GB/s,就算 NPU 算力再猛,带宽不够时依然会被拖住。实测中 7B INT4 在旗舰平台和新款中端平台的差别,远没有想象中大——因为大家都在内存带宽的瓶颈上排队。
  • 功耗墙:GPU 解 LLM 确实快,但峰值功耗很容易破 6W、8W,几秒就烫手。NPU 优势不在“峰值算力”,而在“能效比”。同一道矩阵乘,NPU 的 INT8 算力功耗往往只有 GPU 的零头,这对需要连续出字的对话场景太关键了。

所以我把 DeepSeek 蒸馏小模型视作移动端 NPU 的“标准身材”。参数量、上下文长度、精度损失都控制在一个相对合理的区间,适合被拿来当试验田。

1.2 DeepSeek 这个系列,哪些分支适合端侧

DeepSeek 是一个庞大的模型家族,不是所有版本都能下放到移动端。开源的蒸馏系列里有几个不同参数档位,1.5B、7B 等,还有大参数量的主模型。端侧我们真正值得关注的是蒸馏小参数量分支:它们保留了强推理和指令跟随能力,但参数规模小了一个量级。

顺带提一句,很多人忽略了一个问题:上下文长度。端侧模型就算权重很小,KV Cache 也会随着上下文长度线性膨胀。比如 7B 模型配备 40 层结构,hidden size 在几千,缓存 4K token 可能就要吃掉几百 MB 内存;如果做 32K 长文档,光 KV Cache 就能冲破 2GB。所以部署前先问自己:应用真的需要 32K 上下文吗?大多数助手场景 2K 到 4K 已经非常够用。这个取舍直接决定能不能在 8GB 手机上稳定跑。

1.3 NPU 为什么比 CPU/GPU 更值得押注

手持设备上有三块可编程计算单元:CPU 擅长串行和复杂调度,GPU 擅长大规模并行浮点计算,NPU 则是为“定点矩阵乘加”定制的专用加速器。LLM 的解码阶段里,80% 以上的计算量集中在矩阵乘,而矩阵乘正好是 NPU 的“主场”。只要把算子映射对路,NPU 就能以比 GPU 低得多的功耗完成同样的矩阵乘。

但反过来也要有个预期:NPU 不是万象通用处理器。它对各种“怪算子”的支持非常有限,比如一些非线性激活函数、注意力里的 softmax、某些开方和除法,可能根本没有实现。这部分代码要么走 CPU,要么用等价算子替换。理解这一点,后面才不会被“不支持该算子”的报错弄到怀疑人生。

2. 移动 NPU 的“势力范围”:它擅长什么、卡在什么

如果你之前只在 PC 上用 GPU 跑大模型,第一次接触手机 NPU 会很不习惯。NPU 的编程模型更像一个“保姆式加速器”:你告诉它算子图和张量在哪,它把你的输入切成 tile,逐块搬进内部缓存,算完再搬出来。整个过程高度规律,但灵活性很差。

2.1 核心计算:DSA 专用架构的长处与短板

NPU 内部通常由大量 MAC(乘加)阵列组成,配合深度流水线、大缓存和专用数据搬移引擎。这种 DSA 架构的优点是:在定点计算上效率极高,INT8 算力可以做到同功耗 GPU 的好几倍,而且访存模式固定,无需复杂的指令调度。

短板也来自这种“专用”:

  • 不支持复杂、动态的访存模式。比如 gather、scatter 这类操作,在 NPU 上实现起来效率极低或者干脆不支持。
  • 对非规则 shape 不友好。模型如果频繁改变序列长度,NPU 的编译器和执行引擎就会反复重新 partition,推理延迟出现明显毛刺。
  • 算子之间如果存在大量“小而碎”的张量运算,NPU 的流水线会被打断,性能可能连 CPU 都不如。

在端侧跑 DeepSeek 这类模型时,“破碎算子”是躲不开的。经典例子是注意力机制里的 softmax 和残差连接,这些都是逐元素操作,NPU 也许加速不了,常规做法是在 CPU 上完成,再把中间结果交给 NPU 做下一步矩阵乘。于是推理过程里就出现了一条隐形而重要的“算子分工”——这也是整条部署链路里最容易被低估的部分。

2.2 一张算子支持表值多少钱

动手前,最好先找齐三份材料:NPU 的算子支持列表、推理框架的算子融合能力、模型里实际出现的算子集合。把它们并排放在一起,立刻就知道哪些算子要重写、哪些要改成结构等价的替代方案。

实际操作里我总结了一个简化的算子分类法:

类型代表算子NPU 上的通常待遇处理策略
矩阵运算MatMul、Conv、Gemm强项,直接映射尽量合并成更大算子
张量变换Reshape、Transpose、Concat可能支持但低效能省则省,或放 CPU
逐元素Softmax、LayerNorm、SiLU部分支持,精度敏感算子替换或 CPU 回退
动态控制动态 Shape、条件分支基本不支持固定长度、静态化输入
量化敏感RMSNorm、RoPE量化误差容易被放大部分保持 FP16 或更高精度

很多人看到“矩阵乘能跑”就开心了,结果部署完发现延迟惨不忍睹。实际原因往往是:一个 softmax 回退 CPU 导致张量在两个单元之间来回倒腾,每次拷贝都是几百微秒。算力省下的钱,全被数据搬运烧光了。

2.3 模型结构层面的“NPU 友好化”改造

顺着上面的表,我可以给 DeepSeek 蒸馏模型做一个通用的结构体检:哪些结构是 NPU 的救命恩人,哪些是杀手?

  • 标准 Transformer DecoderLayer 基本是 NPU 友好的:Self-Attention 里的大矩阵乘是主力,MLP 也是纯粹的大矩阵乘。
  • KV Cache 的拼接操作是个隐形坑。每次生成一个新 token,都要把之前的 KV 和新的 K、V 拼起来。如果 NPU 对 Concat 支持得不太好,建议把整块 KV 内存预分配,写入时用“索引定位”而不是反复拼接。
  • RoPE(旋转位置编码)通常需要三角函数运算,NPU 不划算,我会把它提前在 CPU 上算好对应的旋转矩阵,或者干脆在拼接前直接作用于 K、V 张量上,减少 NPU 侧的非矩阵算子。

这些改造不改变模型的数学本质,只改变计算图的表现形式,是工程上最划算的一步。

3. 从权重到能跑:转换、量化、对齐三步走

部署 DeepSeek 到 NPU,我在实践中基本沿一条主线:拿到开源权重,转成中间图格式,再交给端侧推理框架,最后落到 NPU 驱动。每一步都有各自的坑。

3.1 选基底:用什么权重、什么导出方式

先选定参数量。我个人建议从 1.5B 或 7B 的版本开始,不要一上来就挑战 14B。接着要做的第一步是把模型转成通用图格式。为什么这一步不能少?因为 NPU 工具链只认它们定义的算子图,多数平台没有能力直接解析训练框架的 checkpoint 结构。

转换时注意两点:

  • 尽量固定输入尺寸。虽然框架支持动态 batch 和动态序列长度,但 NPU 的预编译器需要先做内存规划,尺寸固定能让它生成更理想的执行计划。
  • 把不需要的训练算子剪掉。导出时务必冻结 BatchNorm、Dropout 这类训练期算子,同时关掉梯度计算,否则图上会残留一堆 NPU 完全不认识的东西。

我在这个环节见过最离谱的报错是:模型的输出只是差了一个平方,最后发现是转换时把某个激活函数“吞掉”了,导致数量级不一致。这里强烈建议导出后,先用 CPU 跑一遍转换图,与原始权重输出做一个逐层 Debug 对比。

3.2 量化:INT8 起步,INT4 看技术耐心

移动端 NPU 默认走定点计算,INT8 是最稳妥的起步点。通过 INT8,7B 模型权重可以从 14GB 降到 7GB 左右,基本上 12GB 内存设备可以跑;继续压到 INT4 则是 3.5GB,能落到 8GB 的机器上。

但 INT4 不是一个免费午餐。常见做法有两种:

  • PTQ(训练后量化):拿几百条有代表性的 prompt 跑一遍模型,统计每个通道的数值范围,再映射到整数。优点是快;缺点是在敏感算子(比如注意力中的 softmax 输入、最后的分类层)上误差会放大。
  • QAT(量化感知训练):把量化误差“模拟”进前向过程,从头微调几个 epoch。效果好,但需要准备训练数据,很多人没有这个条件。

我在实操中的折中方案是“混合精度”:整体 INT8 或 INT4,但对几个特别敏感的算子(RMSNorm、最后的 LM Head)单独保留 FP16。这样做能在内存只增加一点点的情况下,把困惑度损失明显拉低。用滑块验证时,输出质量肉眼可见地变好了。

3.3 和 NPU 驱动的“最后一公里”对齐

转换完了,不等于能直接跑在 NPU 上。端侧推理框架在加载图之后,还会做一次底层算子映射。这里有两种映射结果:

  • 图编译:框架把整个计算图交给 NPU 编译器,一次生成可执行 plan。优点是执行效率高;缺点是如果图里有不支持的算子,整个编译可能直接失败。
  • 算子回退:框架把图拆成几段,支持的段走 NPU,不支持的段自动落到 CPU。日常部署建议开这个模式,既能跑通,又能通过日志看到哪些算子被回退了。

判断是否“跑好”有一个简单指标:看回退算子的个数和出现比例。昨天我自己跑一个 DeepSeek 蒸馏 7B 模型,算子总共有 400 多个,回退到 CPU 的只有十几个,且都是小算子,整体延迟影响在 10% 左右。如果回退比例超过 30%,那基本别指望 NPU 性能了,先去处理图结构吧。

4. 真机调试的实测细节:内存、上下文与分载策略

图能转完、算子能映射,只是“能跑”的起点。真机上的性能又是另外一回事。下面这些细节,几乎每一项都是从反复装机中总结出来的。

4.1 内存池和 KV Cache 的规划

手机上跑 LLM,最怕的不是 CPU 算得慢,而是内存突然吃紧。推理框架默认会在加载模型时申请一整块内存,这块内存不能随便缩小,否则执行时可能出现成套的分配失败。

我的建议是:

  • 加载模型前,先把应用里可释放的缓存全部释放。
  • 为 NPU 预留固定的“工作内存池”,不要让系统 memory manager 频繁参与。
  • KV Cache 提前一次性分配最大长度所需的空间。

比如序列长度上限设为 4096,就把 KV Cache 直接按 4096 分配好。如果只设 2048,生成中途报“KV cache exceeded”,整个对话就要重启。这个例子我踩过,体验极差。

4.2 延迟构成:首次 token 与稳定吞吐

端侧 LLM 有感体验主要看两个指标:

  • 首 token 延迟:从输入到见到第一个字的时间。它包含 prefill 阶段和模型加载、编译、预热等。
  • 生成吞吐:每秒能吐出多少个 token,主要被内存带宽限制。

我在一个集成测试环境里用 7B INT4 在旗舰级移动平台上测得:首 token 延迟约 2.5 到 4 秒,生成速度约 8 到 12 token/s。如果你跑出更差的数字,先别怪硬件,大概率是前文提到的算子回退过多,或者内存带宽分配被其他任务抢了。另一个容易忽略的因素是“保暖问题”:连续推理会让 SoC 温度上升,触发降频,速度可能跌掉一半。持续对话场景必须预留散热阈值。

4.3 多核协同:NPU 主算,CPU 打杂

单一 NPU 跑完整图是最理想的状态,但现实不是。更稳的架构是“NPU 主算,CPU 打杂”:

  • NPU 负责 DecoderLayer 里的大矩阵乘,这也是计算量的大头。
  • CPU 负责 LayerNorm、Softmax、RoPE 等小算子。
  • 数据传输通过共享内存完成,避免无谓拷贝。

这个分工想做得好,需要推理框架支持“异构执行”。好的框架能自动把计算图切成多个子图,分配给不同计算单元。如果框架不支持,就得自己把切分点定在模型结构最“自然”的地方——通常是 DecoderLayer 的边界。切分点选得好,性能提升往往立竿见影。

4.4 带着温度看结果:功耗与热量曲线

聊天应用连续跑三分钟,后盖温度能明显感知。于是我把功耗曲线拆开来看:模型加载瞬间有一段突刺,推理过程中通常稳定在几个瓦特,但连续输出时热量会积累。

功耗优化的几个方向:

  • 给 NPU 设定频率上限,性能换温度。
  • 输出阶段采用“批处理缓存”,让同一批上下文的生成更加连续。
  • 必要时限制输出 token 速度,例如把每秒 10 token 降到 7 token,换回更稳的温度。

实测在 30℃ 环境下,把 NPU 频率上限设在中等档,连续生成 5 分钟,温度能控制在可接受范围内,速度损失可以接受。这个平衡点每个设备不一样,建议做成可配置项,让用户自己权衡。

5. 性能评估:什么样的结果算“部署成功”

部署成功不等于能跑,也不等于跑得快,而是达到了“可用”的基准。我给自己的项目定了一套明确的及格线:

  • 模型加载时间:不超过 5 秒。
  • 首 token 延迟:不超过 4 秒。
  • 稳定生成速度:不低于 6 token/s。
  • 连续工作 10 分钟不因过热退出。
  • 对话输出的语义完整度与云端模型差距不明显。

5.1 我自己的实测数据参考

因为不同机型的 NPU 驱动和内存带宽差异很大,单点数字其实没什么横向参考意义。我把自己环境里的结果列出来,权当给大家一个“量级参考”:

档位参数量量化精度首 token生成速度备注
轻量1.5BINT80.8-1.2s25-40 token/s接近流畅对话
主力7BINT4/混合2.5-4s8-12 token/s可用,但有等待感
极限7BINT83.5-6s5-8 token/s一般不建议日常用

注意,这些结果都是“上一代旗舰平台”上取得的,新平台通常还有 20% 以上的提升空间。

5.2 别只看速度,还要测输出稳定性

跑分只是第一关,更关键的是“输出稳定性”。端到端质量测试时,我会准备三类用例:

  • 知识问答:考察事实性是否会因为量化出现幻觉明显增多。
  • 数学推理:考察多步推理是否崩坏,这是量化误差最敏感的场景。
  • 长文本摘要:考察上下文超过 2K 后,输出是否开始跑偏。

量化模型常见问题是:短文本无感,到长上下文中段就开始逻辑断裂。这通常和 RoPE、注意力计算的数值精度有关。所以我在部署结束后,一定会拿长文本做一轮回归,而不是只看几个 prompt 的结果。这里没有捷径,只能靠数据说话。

6. 绕不开的深坑:动态形状、预热与兼容性

最后这部分,算是我送给后来人的“排雷手册”。任何一条,都能让看似完美的部署计划推倒重来。

6.1 动态形状是 NPU 性能案发现场

Transformer 在推理时,序列长度从 1 到 4096 不断变化。如果计算图把序列长度当成动态维度,NPU 编译器可能做不到静态内存规划,每次都要重新推导开销。我的做法是:

  • 预填阶段用小批处理一次计算完整 prompt。
  • 解码阶段固定 batch=1,把输入长度统一成固定 chunk。
  • 把“生成第 N 个 token”的过程用静态形状的算子子图表达。

这样做之后,性能毛刺明显减少,延迟曲线更平滑。

6.2 模型预热:第一次调用慢到离谱

很多失败案例源于“第一次慢”。第一个问题发过去可能要等十几秒,后面才快起来。原因很简单:NPU 编译和显存初始化都在首次触发。处理办法是启动 App 后立即在后台跑一次“预热推理”,哪怕只生成一个空 token,把编译、内存申请、驱动初始化全部完成。用户真正开始对话时,性能就正常了。

6.3 兼容性矩阵:驱动版本和框架版本锁死

移动端碎片化是老生常谈,但大模型让这个问题变本加厉。同一种 NPU 在不同系统版本上的行为都可能不一样,更别说不同厂商的驱动实现。我的做法是:

  • 把驱动版本、框架版本、模型文件哈希记录进诊断日志。
  • 不可能做到全机型覆盖,重点维护“主力旗舰机型 + 中端热门机型”两条线。
  • 遇到无法解释的崩溃,先把环境版本回退到上一个稳定组合。

这里没有银弹。作为工程人员,只能把“版本锁”和“灰度验证”做扎实,任何一个 NPU 算子实现出现偏差,都会表现为神秘的性能劣化或偶发崩溃。

6.4 端云混合才是更成熟的落地姿势

回到开头说的,DeepSeek 移动到 NPU,真正的落地形态不一定是“所有请求都在端侧完成”。我最后确定的架构是端云混合:

  • 端侧小模型负责短对话、离线场景、敏感隐私数据。
  • 云端大模型负责长文档、复杂推理和高质量生成。
  • 通过上下文压缩和路由规则,把请求智能分流。

这样做的好处很明显:省电、省流、私密,还能在断网时保住基本体验。而端侧 NPU 部署,正是这套架构里最关键的“地面部队”。它不需要取代云端,只需要在离线时刻扛住主阵地。

我在整个项目里最大的体会是:“能不能用 NPU 跑 DeepSeek”其实不是一个二元问题。很多团队做到“能跑”就停了,但真正有价值的反而是后面那两步——能不能跑得稳,能不能在限定功耗里持续输出。如果你也准备入这个坑,建议从 1.5B 起步,先把算子支持表摸透,再一步步往上提参数量。每多跑通一档,你就会更清楚 NPU 的边界在哪里,而这份“边界感”,比任何跑分都值钱。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进