Ollama 模型上下文扩展实战:把 Qwen3.8-27B 从 256K 扩到 1M
背景
最近在本地一台 Jetson AGX Orin 设备(64GB 统一内存)上用 Ollama 跑 Qwen3.8-27B(IQ4_NL 量化,约 16GB)。这代模型原生支持 256K 上下文,官方文档明确说可以通过 YaRN 技术扩展到 1M。
我很好奇这个扩展到底怎么落地——不是用 vLLM 或 SGLang,而是 Ollama 这种”开箱即用”的推理框架。折腾了一晚上,踩了不少坑,记录一下完整过程。
先交代一下环境:一台懒猫算力仓(Jetson AGX Orin 开发套件),Ollama 跑在 Docker 容器里(懒猫 AI Pod 预装的 Ollama 服务,镜像名带 jetson-ollama 字样),模型文件挂载在 SSD 上。
一、前置知识:YaRN 是什么
YaRN(Yet another RoPE extensioN)是一种位置编码扩展技术,可以让训练时上下文有限的模型在推理时处理更长的文本,不需要重新训练。
Qwen3.8-27B 官方 README 给出了明确的 YaRN 配置:
rope_type: yarnfactor: 4.0(从 262144 扩展到 1048576,正好 4 倍)original_max_position_embeddings: 262144partial_rotary_factor: 0.25rope_theta: 10000000
也就是说,模型权重不用动,只要让推理引擎在加载时启用 YaRN 缩放即可。
二、核心思路:改 GGUF 元数据
Ollama 底层是 llama.cpp,而 llama.cpp 会读取 GGUF 文件头部的元数据来决定 rope scaling 行为。所以思路很直接:
- 复制一份现有的 GGUF 文件
- 往元数据里添加
rope.scaling.*字段(type=yarn, factor=4.0, original_context_length=262144) - 把
context_length字段改成 1048576(这一步很关键,原因见下文) - 用
ollama create基于新 GGUF 创建新模型
坑 1:gguf-set-metadata 不能新增字段
llama.cpp 官方提供了 gguf-set-metadata 工具,但它只能修改已存在的字段,新增字段会报 Field not found。而 rope.scaling.* 在原 GGUF 里根本不存在。
解决办法:用 Python 的 gguf 库写脚本,重新生成整个 GGUF。流程是读取原文件 → 复制所有元数据字段和 tensor → 添加新字段 → 写出新文件。16GB 的文件必须流式处理,不能一次性载入内存。
坑 2:tokenizer 数组复制会翻车
tokenizer.ggml.tokens 这类字符串数组,GGUF 里每个元素是”长度前缀 + 内容”两部分。Python 库读出来之后,如果索引取错,会把长度前缀当成字符串内容,导致 tokens 数量翻倍(248320 → 496640),加载时报 Index out of array bounds for toktypes。
还有更隐蔽的:tokenizer 里有些字节不是合法 UTF-8,如果先 decode 再 encode 会直接崩,必须全程按原始字节处理。
坑 3:tensor shape 会被反转
GGUF 的 tensor shape 存储顺序和 numpy 的布局相反。写脚本时如果传错 shape,加载时会报 check_tensor_dims: wrong shape。这个坑不亲自踩一遍很难发现,因为大部分 tensor 恰好不受影响,只有特定结构的(比如 SSM 卷积层)会暴露出来。
坑 4:ollama 会把 num_ctx 钳制在 context_length
新模型创建好之后,我用 API 请求 num_ctx: 1048576,结果 ollama ps 显示 CONTEXT 还是 262144——ollama 的调度器会参考 GGUF 里的 context_length 字段做上限钳制。
解决办法:把 GGUF 的 context_length 元数据也改成 1048576。这相当于告诉 ollama”这个模型支持 1M”,加上 YaRN 字段告诉 llama.cpp”1M 时用 YaRN 缩放”,两边配合才完整。
改完重新 ollama create,ollama show 显示的 context length 就变成 1048576 了。
三、内存才是真正的瓶颈
模型建好了,但实测 1M 上下文加载时进程反复被杀。看日志:
1 | llama_kv_cache: size = 34816.00 MiB (1048576 cells, 16 layers, 1/1 seqs) |
KV cache 直接吃了 34GB!加上模型权重 14GB 和各种缓冲,总需求 55GB 以上,逼近 61GB 物理内存上限。
这台设备上还跑着一个 earlyoom 守护进程(可用内存低于 5% 就主动杀进程),所以进程不是被 OOM killer 杀的,而是被 earlyoom 提前终止的——日志里表现为 signal: terminated 而不是 killed,排查起来更隐蔽。
解决方案:KV cache 量化
KV cache 精度从 q8_0 降到 q4_0,内存直接减半:
1 | llama_kv_cache: size = 18432.00 MiB (1048576 cells, 16 layers, 1/1 seqs) |
总需求降到约 39GB,1M 上下文稳定运行,100% GPU,ollama ps 显示 CONTEXT=1048576。
q4_0 KV 对输出质量的影响很小(KV cache 量化是业界公认的低风险优化),但为了不动懒猫算力仓预装服务管理的原始 compose 文件,我用了 Docker Compose 的 override 机制:
1 | services: |
override 文件放 compose 同目录自动加载,原始文件一行没改,以后懒猫算力仓系统升级也不会冲突。
四、别忘了视觉能力
Qwen3.8-27B 是多模态模型,原模型在 Ollama 里有主模型 GGUF + 两个 CLIP projector(F32 和 F16 版本各一个,tensor 结构完全一样)。
只 FROM 主模型创建的新模型会丢掉 vision 能力。Modelfile 里要把 projector 一起挂上:
1 | FROM <模型存储路径>/blobs/sha256-<主模型> |
挂上之后 ollama show 的 Capabilities 里就有 vision 了,传图测试识别正常。
五、最终结果与限制
最终模型参数:
- 架构 qwen35,27.3B 参数,IQ4_NL 量化,约 16GB
- 上下文:YaRN 扩展,context length = 1048576(1M)
- 视觉:支持(CLIP projector 已挂载)
实测数据:
| 上下文 | KV cache | 总内存 | 状态 |
|---|---|---|---|
| 256K(原生) | ~10GB | ~26GB | 正常 |
| 512K | q8_0 17GB | ~36GB | 正常 |
| 1M | q8_0 34GB | ~55GB+ | earlyoom 杀进程 |
| 1M | q4_0 17GB | ~39GB | 正常 |
一个现实限制:1M 上下文 + 视觉编码同时使用会 OOM(视觉编码需要额外显存缓冲)。实测 512K + 视觉没问题,1M 下纯文本没问题。如果需要 1M 长上下文 + 视觉,得换更大的内存设备,或者把 KV 再压一压。
六、补充:外部客户端接入的隐藏限制
模型调好之后,还有个小插曲。另一台机器把懒猫算力仓的 Ollama 配成了 OpenAI 兼容的推理端点,跑 Hermes 之类的 AI 客户端。客户端在上下文较长时会做”压缩”操作(把历史对话总结成摘要),压缩请求一次会发几万 tokens。
结果压缩一直失败,报错:
1 | request (24697 tokens) exceeds the available context size (8192 tokens) |
排查后发现是第二个”数字陷阱”:Ollama 的 OpenAI 兼容 API 默认上下文是 OLLAMA_CONTEXT_LENGTH 环境变量,默认 8192。客户端走 /v1/chat/completions 时不传 num_ctx 参数(OpenAI 格式里没有这个字段),Ollama 就用 8192 兜底——哪怕模型本身支持 1M。
而我们前面改了 GGUF 的 context_length 元数据,客户端从模型信息里读到”这个模型支持 1M”,于是放心发大请求,结果被服务器的默认值卡住。
修复同样走 compose override,把默认上下文调大:
1 | services: |
调成 128K 是因为:q4_0 KV 下 128K 大约 2GB/seq,内存无压力,同时覆盖了客户端压缩请求的规模(几万 tokens 级别)。如果以后客户端对话更长,再往上调即可;显式传 num_ctx 的请求不受这个默认值影响,仍然可以到 1M。
这个坑的启示:GGUF 元数据说”模型支持多少上下文”是一回事,服务器端实际允许多少是另一回事。Ollama 有两个不同的”上下文数字”:
| 数字 | 位置 | 作用 |
|---|---|---|
context_length(GGUF 元数据) |
模型文件里 | 告诉调度器模型上限,决定 num_ctx 钳制值 |
OLLAMA_CONTEXT_LENGTH(环境变量) |
服务器配置 | OpenAI 兼容 API 的默认上下文,客户端不传参时兜底 |
排查这类问题先分清客户端走到的是哪条 API 路径、有没有显式传 num_ctx,再看对应的那个”上下文数字”。
七、总结
这次折腾的核心收获:
- Ollama 里做 YaRN 扩展 = 改 GGUF 元数据,不需要动模型权重,
rope.scaling.*+context_length两个字段配合 - 16GB 量级的 GGUF 重写必须流式处理,Python 库的 field.parts 布局和字符串数组结构有坑
- 大上下文的真正敌人是 KV cache 内存,q8_0 → q4_0 是最有效的调优手段
- 嵌入式设备的 earlyoom 会让 OOM 排查更难,
terminated不等于崩溃,先查内存守护 - Docker Compose override 是改第三方服务参数的安全姿势,不动原始配置,可随时回退
- 模型支持多少上下文 ≠ 服务器默认允许多少,GGUF 元数据管
num_ctx钳制,OLLAMA_CONTEXT_LENGTH管 API 默认值,两套数字都要对齐
整个过程踩的坑已经整理成内部 skill,下次给其他模型做上下文扩展可以直接复用。
如果对模型上下文扩展感兴趣,可以看看:







