先量设备剩余内存再决定量化档位——Step Edge 端侧部署的取舍

文章导读
已经确定要在本地跑 Step Edge 之后,第一个动作不是挑一个量化档位下载下来试,而是把设备的可用内存和运行时的峰值占用量清楚。量化档位决定权重体积,权重体积决定加载后的峰值内存;如果峰值内存已经贴近上限或触发换页,后面的首字延迟和生成速度对比就没有参考价值了。可以先按“内存筛候选、固定输入比精度、同一段文本测延迟”的顺序走一遍,每次换档位只重装一次,而不是每换一次都靠手感判断。
📋 目录
  1. 一 把部署目标拆成内存、延迟、精度三条可测指标
  2. 二 按内存上限先选出可行的量化档位候选集
  3. 三 用同一组固定输入对比各档位的输出
  4. 四 在同一段文本上测首字延迟与持续生成速度
  5. 五 把结果写进部署配置并记录选择理由
A A

已经确定要在本地跑 Step Edge 之后,第一个动作不是挑一个量化档位下载下来试,而是把设备的可用内存和运行时的峰值占用量清楚。量化档位决定权重体积,权重体积决定加载后的峰值内存;如果峰值内存已经贴近上限或触发换页,后面的首字延迟和生成速度对比就没有参考价值了。可以先按“内存筛候选、固定输入比精度、同一段文本测延迟”的顺序走一遍,每次换档位只重装一次,而不是每换一次都靠手感判断。

在 Step Edge 端侧部署里,量化档位应当由测量结果倒推:先读可用内存,用权重体积算式筛出能加载的候选集,再用同一组固定输入比较输出差异,并在同一段文本上记录首字时间与生成速度的中位数。适用场景是单机自用、可以反复重装的环境;如果设备内存余量本来就紧,优先保住峰值内存、宁可退一档,不要靠 swap 把低内存档位硬撑起来。

把部署目标拆成内存、延迟、精度三条可测指标

先定清楚测什么,才不会被“感觉还行”带偏。三条指标各给一句话定义和一种落地测法:

  • 内存:进程在完成一次完整生成过程中的峰值常驻内存(RSS 峰值),不是刚启动时的占用。测量方式是看 /proc/<pid>/status 里的 VmHWM,或者用 /usr/bin/time -v 包住启动命令读 Maximum resident set size。
  • 延迟:拆成首字时间和持续生成速度两段,前者是从发起请求到第一个 token 返回,后者是首字之后到生成结束的 token 产出速率。测量方式是在客户端打时间戳,同一输入重复若干次取中位数。
  • 精度:在固定提示词、固定输入长度、固定生成参数下,各档位输出与高精度档位输出的差异。测量方式是逐档保存完整输出,按场景关心的点逐条比对,而不是看单条回复“像不像人话”。

内存这一步可以先用两条命令建立基线,替换掉你自己的启动命令即可:

grep -E 'MemTotal|MemAvailable' /proc/meminfo
/usr/bin/time -v ./run_step_edge `--model` ./step-edge `--quant` <候选档位> 2>&1 | grep -E 'Maximum resident'
# 进程已经在跑时,读峰值而不是当前值
grep VmHWM /proc/$(pgrep -f run_step_edge)/status

用 MemAvailable 而不是 MemFree 做判断更稳妥,因为后者会把可回收的页缓存算成已用。峰值内存要覆盖一次完整生成,包含长上下文时的中间激活,所以测量时把输入长度拉到目标场景的上限再读 VmHWM。

按内存上限先选出可行的量化档位候选集

这一步的目的是缩小试验范围,减少重装次数。从可用内存反推可加载的权重体积,可以先写成一个粗略算式:

可用于权重的字节 ≈ MemAvailable − 运行时框架常驻 − 中间激活峰值 − 系统余量
每权重平均位数 ≈ 可用于权重的字节 × 8 ÷ 模型参数量

算出来的“每权重平均位数”一般不会正好落在某个整数档位上,用它去卡一个区间即可:算出来明显高于 8 位,说明高精度档位也在范围内;落在 4 到 8 位之间,就优先试 4 位档和中间档;算出来低于 4 位,要么缩短上下文、要么接受更激进的量化,要么换设备。中间激活峰值随上下文长度增长,这一点需要单独测——按短输入的峰值去选档位,长输入时很容易爆。

先量设备剩余内存再决定量化档位——Step Edge 端侧部署的取舍

先按不超过可用内存的一半左右试算权重体积,再留出回调空间,是个保守起点,具体比例还是要结合运行框架实测的常驻开销确认。候选集通常控制在两到三个档位,超过三个就意味着你还没把内存账算清楚。

用同一组固定输入对比各档位的输出

精度对比要可复现,关键是固定住输入和采样参数。下面是一个可以直接改用的固定题骨架,把正文换成你目标场景里的真实样本,长度保持一致:

系统提示:你是一个信息抽取助手,只输出原文中出现过的信息。
用户输入:把下面这段文本概括为三条要点,并列出其中出现的机构名与日期。
正文:(固定一段约 N 字的文本,各档位使用完全相同的字节内容)
生成参数:temperature、top_p、max_tokens 各档位保持一致
判分方式:逐条比对三条要点是否齐全、机构名与日期是否抄错、是否出现原文没有的内容

记录差异时看这几类可观察现象:输出是否被截断、要点数量是否变少、专有名词或数字是否写错、是否出现重复循环、中英文是否混杂。这些都能从保存下来的文本里直接看出来,不需要主观打分。

“够用”的判定要绑定场景:如果目标是摘要或分类,措辞变化可以接受,但关键实体不能丢;如果是抽取结构化字段,要求字段齐全且能被解析器直接解析;如果是开放式对话,容忍度可以放宽。把判定标准写在对比表旁边,后面换档位时才有共同口径。

在同一段文本上测首字延迟与持续生成速度

延迟数据要变成选档位的硬约束,前提是计时点定义一致。建议统一定义三个时刻:发起请求(客户端发出请求的时刻)、首字返回(收到第一个流式片段的时刻)、生成结束(收到结束标记的时刻)。首字时间 = 首字返回 − 发起请求;生成时长 = 生成结束 − 首字返回;生成速度 = 该段产出的 token 数 ÷ 生成时长。

先量设备剩余内存再决定量化档位——Step Edge 端侧部署的取舍

同一输入重复跑若干次取中位数,避免第一次运行的冷启动把结论带偏。下面是一个通用的打点骨架,流式调用换成你所用量化运行时的接口即可:

import time, statistics

def once(send_stream):
    t0 = time.perf_counter()
    first = None
    tokens = 0
    for chunk in send_stream():
        if first is None:
            first = time.perf_counter()
        tokens += 1
    end = time.perf_counter()
    ttft = first - t0
    speed = tokens / (end - first) if end > first else float('nan')
    return ttft, speed

ttfts, speeds = [], []
for _ in range(5):
    a, b = once(send_stream)
    ttfts.append(a); speeds.append(b)

print('首字中位数', statistics.median(ttfts))
print('速度中位数', statistics.median(speeds))

如果运行时没有返回精确的 token 计数,用字符数近似也可以,但各档位必须用同一种口径,否则速度数字之间不可比。测延迟时把线程数也固定住,否则你测到的是线程配置的差异,不是量化档位的差异。

把结果写进部署配置并记录选择理由

选定档位后,把它和对应的线程参数写在同一份配置里,字段名以你所用运行时为准,下面只是示意位置:

# config.yaml —— 字段名按你的运行框架替换
model:
  path: ./step-edge
  quant: q4            # 与候选集里的档位命名对齐
runtime:
  threads: 4           # 与测延迟时保持一致
  context_length: 2048 # 与测峰值内存时的输入上限一致

配置旁边配一张对比记录表,和配置一起提交进仓库,换设备或升级运行时之前先复测这三条指标。表至少包含这些字段:

字段记录内容
档位名与权重体积候选集里的档位标识、磁盘上的权重文件体积
运行参数线程数、上下文长度、可用内存快照
峰值内存VmHWM 读数,注明输入长度
延迟首字时间中位数、生成速度中位数、重复次数
精度结论固定题目上的输出差异类型,以及是否满足场景判定标准
选择理由为什么选这一档、放弃了哪一档、放弃的原因

选择理由写成可核查的句子,例如“峰值内存接近可用上限,切到更长上下文时余量不足,因此退一档”;或者“输出在固定题目上丢失机构名,不满足字段抽取要求”。这样的记录能让下一个换设备的人直接看出约束在哪里,而不是重新从零试错。