Llama 3 本地对话首字很慢 / 是上下文太长还是显存带宽不够?

文章导读
首字要等很久、后面出字还算正常,说明时间主要花在预填充阶段:模型先读完你这一整段上下文、算完注意力、把 KV cache 建好,才吐出第一个 token。预填充和之后的逐字解码是两种不同的负载,前者更吃算力与上下文长度,后者才更直接受显存带宽影响。所以「是不是上下文太长」和「是不是显存带宽不够」不能凭感觉回答,得先把两个数分开量。
📋 目录
  1. 先量出首字等待时长与后续生成速度两个数
  2. 缩短上下文长度做单变量对照
  3. 检查是否发生显存不足导致的层卸载
  4. 在同一台机器上比较两个量化档位
  5. 把参数固定成一份可复现的对话配置
A A

首字要等很久、后面出字还算正常,说明时间主要花在预填充阶段:模型先读完你这一整段上下文、算完注意力、把 KV cache 建好,才吐出第一个 token。预填充和之后的逐字解码是两种不同的负载,前者更吃算力与上下文长度,后者才更直接受显存带宽影响。所以「是不是上下文太长」和「是不是显存带宽不够」不能凭感觉回答,得先把两个数分开量。

首字慢多半是预填充慢,而不是解码慢,先别急着换卡。用同一段提示词分别记录首字等待(TTFT)和每秒输出 token 数,再做三组单变量对照:缩短实际输入 token 数、把 GPU 层数从部分卸载调到尽量全部卸载、同一上下文长度下比较两个量化档位。若 TTFT 随输入长度接近线性上升,优先缩上下文;若启动日志显示有层被丢到 CPU,先调层数;若换更小量化后 TTFT 几乎不动,瓶颈通常不在显存带宽,而在算力或固定调度开销。结论以本机复测为准。

先量出首字等待时长与后续生成速度两个数

最省事的计时方式是用客户端时间戳:发出请求前记一次,看到第一个 token 打印出来再记一次。命令行下可以先记 date +%s.%N,两次相减就是 TTFT。如果本地推理程序自带日志,通常已经把两个阶段分开打了。

以 llama.cpp 的命令行程序为例:

./llama-cli -m llama3-8b-instruct-q4_k_m.gguf \
  -p "用三句话介绍你自己" \
  -n 64 -c 4096 -ngl 99 `--verbose-prompt`

运行结束后看日志里两个字段:prompt eval time 对应预填充,基本就是首字等待的主体;eval time 对应逐字解码,用它除以生成的 token 数就是每秒输出量。用 Ollama 的话,`--verbose` 会打印 prompt eval durationeval rate,语义一样。

轮次TTFT(秒)prompt token 数输出 token 数解码 tok/s显存占用
1(冷启动,含加载)
2
3
中位数

记录时注意三件事:第一次运行包含模型加载,属于冷启动,要么单独记要么丢弃;同一配置至少跑 3 到 5 次取中位数,不要拿单次最好成绩当基准;每次都要把 prompt token 数写下来,否则后面几组对照没法比。

缩短上下文长度做单变量对照

这一步验证的是:首字耗时是否随实际输入 token 数增长。注意 -c 只是 KV cache 容量的上限,把它从 8192 改成 2048 并不会自动缩短你的输入,真正要变的是喂进去的文本长度。准备一份长文本和一句短提示词,其他参数全部不动:

# 长上下文
./llama-cli -m llama3-8b-instruct-q4_k_m.gguf -c 8192 -ngl 99 \
  -p "$(cat long_ctx.txt)" -n 64

# 短上下文,同一个问题
./llama-cli -m llama3-8b-instruct-q4_k_m.gguf -c 8192 -ngl 99 \
  -p "用三句话介绍你自己" -n 64

两次都让它把同样长度的内容说完,比较 TTFT、prompt token 数和显存占用。开另一个终端同步观察显存变化:

Llama 3 本地对话首字很慢 / 是上下文太长还是显存带宽不够?
nvidia-smi `--query-gpu`=memory.used `--format`=csv -l 1
场景prompt token 数TTFT(秒)峰值显存解码 tok/s
长文本
短提示词

如果长文本那次的 TTFT 明显更高,甚至大致按 token 数成比例上升,说明首字时间主要被预填充吃掉,缩短历史记录、减少检索片段、精简系统提示词都是有效方向。如果长短两次 TTFT 差不多,别在这条线上继续折腾,去看层卸载。

检查是否发生显存不足导致的层卸载

显存不够时,推理程序会把一部分层留在内存里交给 CPU 算,首字时间会被明显拖长,而且这种现象不会因为换成更小的模型就一定消失。llama.cpp 启动日志里通常能看到类似 load_tensors: offloaded 20/33 layers to GPU 的行,斜杠右边是模型实际的层数,左边是真正上了 GPU 的层数。两边不相等,说明有层被卸载了。Ollama 对应的参数是 num_gpu

把层数从部分卸载调到尽量全部卸载,再比一次:

./llama-cli -m llama3-8b-instruct-q4_k_m.gguf -c 4096 -ngl 20 -p "用三句话介绍你自己" -n 64
./llama-cli -m llama3-8b-instruct-q4_k_m.gguf -c 4096 -ngl 33 -p "用三句话介绍你自己" -n 64

-ngl 的具体上限以启动日志报的层数为准,给一个超过总数的值一般会被自动截断。对照时记录下面几项:

ngl日志中 offloaded 比例TTFT(秒)峰值显存解码 tok/s
20
33(或全部)

如果调大 -ngl 后首字明显变快,同时显存还没顶满,那之前的慢就是卸载造成的。如果显存已经到上限、再调就报分配失败,说明这张卡的容量装不下这个模型加这段上下文,只能降量化、缩上下文,或者换显存更大的卡。

Llama 3 本地对话首字很慢 / 是上下文太长还是显存带宽不够?

在同一台机器上比较两个量化档位

量化档位是判断瓶颈落在算力还是带宽上最省事的对照手段。准备两个参数量相同、量化不同的模型文件,比如 Q4_K_M 和 Q8_0,放在同一目录,提示词、上下文长度、GPU 层数全部保持一致,只换 -m

./llama-cli -m llama3-8b-q4_k_m.gguf -p "用三句话介绍你自己" -c 4096 -ngl 99 -n 64
./llama-cli -m llama3-8b-q8_0.gguf   -p "用三句话介绍你自己" -c 4096 -ngl 99 -n 64
量化档位文件大小TTFT(秒)峰值显存解码 tok/s
Q4_K_M
Q8_0

判据这样读:更大的量化文件需要搬运更多权重,对显存带宽和容量的压力都更大。如果换到小量化后 TTFT 几乎不变,说明首字等待不是被显存带宽卡住的,更可能是预填充算力、batch 设置或每轮固定调度开销;如果小量化 TTFT 明显下降、而解码速度基本持平,说明带宽确实参与了首字阶段,可以继续用更小的量化,或者考虑带宽更高的卡。如果两个量化都随输入长度线性变慢,回到上一节缩上下文,而不是换硬件。

把参数固定成一份可复现的对话配置

把上面几轮对照里表现最稳的一组写下来,下次直接套用,也方便以后复测时对比。下面是一份通用骨架,数值按你自己机器上的结果填:

model:    llama3-8b-instruct-q4_k_m.gguf
ctx:      4096
ngl:      全部卸载(以启动日志 offloaded N/N 为准)
threads:  物理核心数
batch:    512     # prompt batch,影响预填充速度
ubatch:   512     # 显存吃紧时可适当调小
prompt:   固定一句测试用提示词
max_tokens: 64

硬件与运行环境也要一起记,否则换台机器后数字没有可比性:

nvidia-smi `--query-gpu`=name,driver_version,memory.total `--format`=csv
./llama-cli `--version`

复测顺序建议固定成:确认当前没有别的进程占用显卡,先跑一次热身并丢弃结果,再用同一提示词连跑 3 次,分别取 TTFT 中位数和每秒输出 token 中位数。这两个指标里,TTFT 回答的是「首字为什么慢」,解码速度回答的是「后面顺不顺」,任何一次调参后都要两个一起看,只改善其中一个不足以说明问题解决。

还有一个边界要说清楚:显存不足时启用的换页、把模型临时挪到内存这类做法只是让程序还能跑起来,不是提速手段,它们通常会让首字更慢。真正要不要换卡,取决于缩上下文和调层数之后 TTFT 是否还压不下来。