Ollama 上下文怎么设:4096 实测只吃 2050 token

一句话结论:Ollama 0.40.14 在 16GB 统一内存的 Mac 上,num_ctx 写 4096 时模型真正能读到的输入只有 2050 个 token,另一半被留给了输出,超出的部分从开头直接丢掉,接口照回 200。长文档问答里模型答得离谱,先量这一项,再怀疑模型本身。

4096 只给 2050:四档实测的输入上限

测试机是 M4 16GB,Ollama 0.40.14,模型 qwen3:8b(Q4_K_M,训练上下文 40960)。每档都先卸载再重新加载,输入是一段 16807 个 token 的日志文本,开头埋了一串唯一校验口令,末尾问口令是什么。Ollama 每次截断都会在服务端日志写一行 WARN,把上限和原始长度一起打出来,下表的上限就是从这行日志读的。

num_ctx 日志里的输入上限 实际吃进的 token 问口令的回答
2048 1026 1026 答「未找到相关内容」
4096 2050 2050 答「文本里未包含」
8192 4098 4098 编出一串口令
16384 8194 8194 编出另一串口令
24576 未触发截断 16807 PURPLE-4072 正确
32768 未触发截断 16807 PURPLE-4072 正确

表里最值得看的是后两列。2048 和 4096 两档,模型老老实实说没看到;8192 和 16384 两档,模型给出的却是两串看着很像口令的东西,实测分别是 QWERTYUIOPASDFGHJKLZXCVBNM 和 123456。截断发生在开头,档案头被切掉之后,模型面对一个「去前面找口令」的问题,找不到就顺着问句编一个。同样是失败,前两档你一眼能看出来,后两档看不出来。

量你自己的机器:三条命令

第一条看默认分档。Ollama 0.40 起不再固定 4096,改成按显存自动分档,启动时打一行日志,直接 grep 出来:

grep "vram-based default context" ~/.ollama/logs/server.log

这台机器输出 total_vram=”11.8 GiB” default_num_ctx=4096。ollama serve --help 里写的是按 VRAM 给 4k/32k/256k 三档,11.8GiB 落在 4k 档,要 24GiB 以上才会给 32k。

第二条看当前加载状态。ollama ps 的输出比老版本多了一列 CONTEXT,直接显示真实上下文。要写脚本就读 GET /api/ps 的 JSON,里面有 context_length 字段,比解析表格稳。

第三条看底层启动参数。ps aux | grep llama-server 能看到 Ollama 拼给 llama.cpp 的完整命令行,其中 -c 是上下文长度,-np 是并行槽数,--keep 4 是截断时保留的 token 数,--context-shift 说明它丢旧内容用的是滑窗。这三处只要有一处和你以为的不一样,问题就在这里。

为什么只剩一半

Ollama 给输出预留了接近一半的上下文。4K 档留出 2046 个 token 做生成,8K 档留 4094,16K 档留 8190,都是一半减二。到 24K 以上规则松了,16807 个 token 的输入在 24576 档和 32768 档都能整段进去。所以想要 X 个 token 的输入,num_ctx 至少给 2X,16K 以内这条换算都成立。

如果你是通过 DeepSeek Harness 接本地 Ollama,那篇处理的是客户端写 8192 的问题,本文量的是服务端到底给了多少,两边叠在一起才会出现莫名的崩溃。底层机制上,llama.cpp 的 -c 参数就是上下文总量,Ollama 在它外面又切了一刀,这也是 直接用 llama.cpp 跑 GGUF 时上下文更好控制的原因。

内存账:每 1K 上下文 152MB

多给上下文的代价是可量的。同样 qwen3:8b,五档 num_ctx 加载完的占用如下,全部 f16 KV,未设 flash attention。

num_ctx 加载后占用 相对上一档每 1K 的增量
2048 4.77 GB 基准
4096 5.08 GB +152 MB
8192 5.68 GB +152 MB
16384 6.92 GB +154 MB
32768 9.93 GB +188 MB
16384 加 q8_0 KV 5.79 GB 省 1.13 GB

前两档的增量完全线性,152MB 这个数能对上 qwen3:8b 的结构:36 层、8 个 KV 头、每头 128 维、K 和 V 各存一份、f16 两字节,算下来每个 token 145KiB,llama.cpp 对齐之后是 152KB。过 16K 之后每 K 涨到 188MB,多出来的是注意力计算缓冲。KV 估算和实测为什么会对不上,这篇算过一次差值,结论同源。

KV 缓存量化能直接砍这一块。同样是 16384 上下文,把 KV 换成 q8_0,占用从 6.92GB 降到 5.79GB,正好省一半。设置方法:

launchctl setenv OLLAMA_KV_CACHE_TYPE q8_0
launchctl setenv OLLAMA_CONTEXT_LENGTH 16384
# 退出 Ollama 应用后重新打开,或直接起前台实例
OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_CONTEXT_LENGTH=16384 ollama serve

设了量化类型之后 flash attention 会自动打开,日志里有 enabling flash_attn since it is required for quantized V cache,不用另外加参数。启动命令行里也能看到 --cache-type-k q8_0 --cache-type-v q8_0 --flash-attn auto。

三种改法,优先级从高到低

请求级最优先。调用 /api/generate 或 /api/chat 时把 num_ctx 放进 options,注意是 options 里面:

curl http://127.0.0.1:11434/api/generate   -H 'Content-Type: application/json'   -d '{"model":"qwen3:8b","prompt":"你的长文档","stream":false,
       "options":{"num_ctx":32768,"num_predict":256}}'

模型级次之。写一个 Modelfile 再 create 一份,之后不管哪个客户端连过来,默认就是这个数:

# Modelfile
FROM qwen3:8b
PARAMETER num_ctx 16384

ollama create qwen3-8b-16k -f Modelfile
ollama run qwen3-8b-16k

服务级最后兜底。OLLAMA_CONTEXT_LENGTH 决定的是没人指定时用多少,设成 16384 之后所有模型默认 16384,实测加载出来的 context_length 就是 16384。

三者叠加谁生效,逐组测了一遍。Modelfile 写 16384 且请求不传,加载出来 16384;请求再加 num_ctx 8192,加载出来 8192;请求不传而服务端有环境变量,按环境变量走。顺序是请求大于模型大于服务端。

最容易白忙的一处:/v1 接口根本不认 num_ctx

大量客户端走的是 OpenAI 兼容端点 /v1/chat/completions,不走 Ollama 原生端点。这个路径上传 num_ctx 等于没传。实测两次,第一次放在请求体顶层,第二次包进 options,两次都返回 200,去 /api/ps 看 context_length 仍然是 4096,一个字都没变。

原因很直接,OpenAI 的请求格式里没有上下文字段,Ollama 也不从那里读。走 /v1 的客户端只有两条路:改服务端环境变量,或者先用 Modelfile 把模型烤成更大的上下文,再让客户端连那份。另外 OLLAMA_NUM_CTX 这个名字是旧教程里的写法,0.40.14 的服务端完全不读它,改这个变量只会白等。

踩坑清单

第一个坑是把 num_ctx 放到请求体顶层。它不算错也不算对,会被静默忽略。JSON 里 options 是唯一认的位置,错一层,接口照样 200,模型照样用默认档。

第二个坑是拿 24K 以上的档位按一半倒推。24576 档能把 16807 个 token 整段吃下,因为到了这个高度预留规则换了算法。死守一半会把内存多花几 GB,也会把本来跑得动的文档误判成跑不动。

第三个坑是把日志里的 default_num_ctx 当现状。OLLAMA_CONTEXT_LENGTH 设成 16384 之后,模型加载出来确实是 16384,但启动日志仍然打印 default_num_ctx=4096,这行只是 VRAM 分档的推荐值,不反映你的覆盖。判断真实值只能看 ollama ps 或 /api/ps。

该设多少

16GB 统一内存的机器配 8B 模型,16384 是舒适档,占用 6.92GB,还留着空间给系统和别的进程。32768 也能起来,占用 9.93GB,代价是提示词处理速度从 205 token/秒 掉到 147 token/秒,16807 个 token 的输入要等 114 秒才出第一个字。时间账在这里:1026 token 用 4.6 秒,8194 token 用 45.8 秒,16807 token 用 114.4 秒,上下文越长单位成本越高。

整段文档问答这类场景,num_ctx 给到文本长度的两倍以上,再配 q8_0 压住内存。日常对话 8192 够用,设太大只是让首字变慢。还要注意并发槽和上下文抢同一块内存,几个槽各分一份 KV,和 NUM_PARALLEL 那篇的实测叠起来算,才能真正估准一台机器能同时扛几个请求。

FAQ

问:num_ctx 设 32768,是不是就能吃 32768 个 token 的输入?
不是。16K 以内只能用一半,32K 档实测能吃下 16807 个 token,比例略高于一半。要保证两万 token 的文档整段进去,把 num_ctx 设到 40960,那是 qwen3:8b 的训练上限,再高只是白占内存。

问:为什么模型答得离谱,接口却没报错?
截断是服务端的静默行为,200 照回,body 正常。判断办法是看返回里的 prompt_eval_count,它等于本次真正送进模型的 token 数,明显小于你的文本长度就是被截了。也可以直接 grep 日志里的 truncating input prompt,那一行会同时给出上限和原始长度。

问:RAG 和知识库问答,num_ctx 还要调吗?
要。检索回来的片段加上历史对话很容易过 4096。更稳的做法是把召回片段的 token 数卡在 2000 以内,num_ctx 给 8192,让每一段都完整进入模型,而不是靠模型自己从被截断的残文里猜。