num_ctx是KV cache的预分配slot数,不是模型强制的上下文限制;实际容量受训练长度和内存双重约束,超出训练长度会被静默截断。
num_ctx 不是模型强制执行的限制。它是一个内存分配:Ollama 启动 runner 时在 key-value 缓存中保留的 token 槽位数量。关于这个参数的所有困惑——模型遗忘、单独加载成功但并发就失败、设置静默地没有生效——都源于这一事实。
当请求到达时,Ollama 启动一个 llama.cpp server 进程并传递一个 context size。这个大小决定了在处理第一个 token 之前预先分配多大空间的 KV 缓存。这个分配不是惰性的,也不会增长:一个以 num_ctx 为 32768 启动的 runner,已经为 32,768 个 token 槽位付费了,无论你的 prompt 是 4 个 token 还是三万个。
两个上限位于你设置的数字之上。模型训练时设定的 context 在 GGUF 元数据中有记录,这构成了第一层上限——请求 256k 但模型只支持 32k 时,Ollama 会 clamp 到文件中的值而不是报错。内存则是更严格的第二层限制:缓存放不下 VRAM 时,层会溢出到系统 RAM,此时模型运行速度取决于 PCIe 总线带宽。这种溢出在 API 层面是静默的,但通过 ollama ps 可以观察到,其 PROCESSOR 列会报告类似 48%/52% CPU/GPU 这样的拆分。
缓存大小完全由模型发布的数字决定。对于每个 token,运行时为每层存储一个 key 向量和一个 value 向量,大小由 key-value heads 数量乘以 head dimension 决定。所以:
bytes_per_token = 2 (K and V)
x n_layers
x n_kv_heads
x head_dim
x bytes_per_element
total_kv_bytes = bytes_per_token x num_ctx x num_parallel
以 Qwen3-8B 为例,阿里巴巴在 Hugging Face 上发布的 config.json 给出了 num_hidden_layers 36、num_key_value_heads 8 和 head_dim 128。在 f16 下,每个元素两个字节:
2 x 36 x 8 x 128 x 2 = 147,456 bytes per token
= 144 KiB per token
num_ctx = 4,096 -> 576 MiB
num_ctx = 32,768 -> 4.5 GiB
num_ctx = 262,144 -> 36 GiB
这是来自发布配置的计算,而非实测,这也是为什么将默认设置从 4k 跳到 256k 在笔记本上不是可以随意做出的决定。注意公式中没有出现什么:query heads。分组查询注意力意味着在这个模型上 32 个注意力头共享 8 个 key-value 头,所以缓存是多头模型相同宽度配置的四分之一。如果 num_key_value_heads 等于 num_attention_heads,在相同 context 下每个 token 的成本要高出四倍。
Ollama 的 Modelfile 参考文档仍然列出 num_ctx 的默认值为 2048。服务器实际上已经有一段时间没有使用固定默认值了。Ollama 的上下文长度文档说明了当前的行为:默认值是根据检测到的 VRAM 来选择的,低于 24 GiB 时为 4k,24 到 48 GiB 之间为 32k,48 GiB 及以上为 256k。服务器源代码使用的阈值比文档描述的略低——分别是 23 和 47 GiB——有一个注释说明这个边距是为了吸收报告总数中的小差异。
自动选择只在没有人发表过意见时才会应用。在请求选项中设置 num_ctx,或在模型的 Modelfile 中设置,或将 OLLAMA_CONTEXT_LENGTH 设置为任何非零值,都会使该加载完全脱离层级选择。这就是常见问题报告背后的机制:用户在 Modelfile 中设置了 131072,模型不再适合,之前成功的加载现在会溢出到 CPU——因为保护他们的自动调整大小不再运行。
自动路径还有第二部分。当 Ollama 自己选择了上下文但加载耗尽内存时,它会逐步降低到下一个较低的层级重试——从 256k 到 32k,从 32k 到 4k——低于这个则放弃。手动的上下文设置不会获得这种重试机制:它会失败,或者会溢出。这些层级和阈值在编写时是当前的,并且至少已经更改过一次。
并发会与之相乘。Ollama 将 num_ctx x num_parallel 作为服务器的上下文总量传递,并将 num_parallel 作为槽位数量,所以提高任一个都会增加分配。Ollama 的 FAQ 直言不讳:所需内存按 OLLAMA_NUM_PARALLEL 乘以 OLLAMA_CONTEXT_LENGTH 的比例缩放。参见 num_parallel 和并发请求。
缓存精度会将其减半。当 flash attention 启用时,OLLAMA_KV_CACHE_TYPE=q8_0 以 f16 默认值大约一半的字节数存储缓存,q4_0 则大约是四分之一。Ollama 将其记录为影响每个模型的全局设置,并警告具有高分组查询比率的模型会因此损失更多精度。
预填充时间随着你发送的内容增长,而不是随着你保留的内容增长。较大的 num_ctx 会立即消耗内存,只有在实际填充时才会消耗时间。注意力工作的增长速度比存在的 token 数量快得多,所以一个真正填满的 128k 上下文会因为与分配无关的原因而变慢。
溢出行为是一个独立的决定。当对话超出窗口时会发生什么——截断最旧的轮次、移动缓存或拒绝——是在其他地方控制的,这就是上下文长度错误可能出现在你设置的数字以下的原因。
有四个地方可以设置它,它们有不同的生命周期。按请求,在 JSON body 中的 options.num_ctx 只适用于该次加载。交互式 REPL 中,/set parameter num_ctx 16384 适用于该会话。在 Modelfile 中,PARAMETER num_ctx 16384 会随模型传递给拉取它的任何人。在服务器范围内,OLLAMA_CONTEXT_LENGTH=64000 ollama serve 更改默认值,必须在服务器实际读取环境的地方设置——Linux 上是 systemd drop-in,macOS 上是 launchctl setenv,Windows 上是用户环境。在运行客户端的 shell 中导出它什么都不会改变。
验证而不是假设。ollama ps 打印一个 CONTEXT 列,显示加载的模型实际获得的上下文,这是唯一重要的数字,通常不是你要求的那个。
尴尬的情况是应用程序的上下文需求是双峰的:几乎每个请求都适合 8k,有几个需要六位数。将本地 runner 调整为尾数会在每个普通请求上浪费内存,将其调整为中位数意味着长的请求会失败。将溢出路由到托管的长上下文模型意味着一个调用点处理两个后端,每个后端有不同的限制和不同的错误形状;像 Multigrid 这样的网关在一个请求格式后面规范化这些,所以 fallback 是一条路由规则而不是第二个客户端。