十年老服务器跑千问 3.5 4B:被硬件逼出来的调参实录

0 阅读9分钟

先说结论:一台十年前的机器,能跑、能对外提供服务,但整个过程不是"装完就完事"——这代硬件(AMD Polaris 显卡 + 没有 AVX2 的 Ivy Bridge CPU)逼出了三个反直觉的决定:关掉 flash attention、接受 CPU 当主力、用固定别名把参数钉死。

环境:Intel Xeon E5-2670 v2 / 32 GB DDR3L / AMD RX 580 2048SP 8 GB / Windows 10 专业版 19045,跑 Ollama 0.40.14 + 千问 3.5 4B(Q4_K_M),给局域网内的 WorkBuddy、Dify 等平台提供 OpenAI 兼容 API。

一、硬件直接决定了方案边界

项目实际配置对方案的影响
CPUXeon E5-2670 v2,10 核 20 线程,Ivy Bridge,仅 AVX 无 AVX2只能当兜底算力,预填充约 54~74 tok/s
内存32 GB DDR3L-1600撑得住 9.6 GB 的模型 + KV 占用
显卡RX 580 2048SP / 8 GB,Polaris gfx803ROCm 不支持(只支持 RX 7600 起的 RDNA3),只能走 Vulkan
系统Windows 10 19045Ollama 0.40.x 默认启用 Vulkan 后端,Polaris 可用

三条硬边界:

  1. 不能上 ROCm,只能 Vulkan。 Vulkan 在 Polaris 上对"非标准注意力结构"支持不全,这是后面所有取舍的根源。
  2. 8 GB 显存决定模型上限。 4B Q4(3.3 GB)宽裕,8B Q4(约 5 GB)临界,14B 以上必须 CPU 混合、速度崩到 1~2 tok/s。
  3. 模型选型定死。 能用的就是 Qwen3.5 4B(混合架构)。换纯稠密模型(如 qwen3:4b-instruct-2507)虽然更上卡,但实测会随机输出整串 0 的静默缺陷,已放弃并删除。

顺带一提:本机还有个向日葵虚拟显示器 OrayIddDriver Device,没卸载——它不参与 Vulkan 计算,删了没收益,还可能影响远程连接。

二、最终配置长什么样

对外只有一个名字:qwen-api。所有平台都填这个模型名,底层换模型只重建别名,调用方代码不用改。

FROM qwen3.5:4b-q4_K_M

PARAMETER num_ctx 16384
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER repeat_penalty 1.05
PARAMETER num_predict 2048

SYSTEM "你是运行在这台 Windows 电脑上的本地大模型助手。回答简洁、准确,默认使用中文;不确定时直接说明不确定,不要编造事实。"

用户级环境变量(Ollama 只在启动时读一次,改完必须重启服务):

变量值作用
OLLAMA_HOST0.0.0.0:11434监听所有网卡,否则局域网其他机器连不上
OLLAMA_KEEP_ALIVE-1模型永久常驻,不主动释放
OLLAMA_MAX_LOADED_MODELS18 GB 显存只容得下一个模型
OLLAMA_NUM_PARALLEL2名义 2 路并发,实测被压到 1 槽
OLLAMA_FLASH_ATTENTIONfalse关键反向设置
OLLAMA_CONTEXT_LENGTH16384兜底值,实际以 Modelfile 的 num_ctx 为准

服务用启动脚本拉起(脚本里先设好变量再启动),避免"改了环境变量却没生效"这个高频坑。没做开机自启,重启后手动跑一次。

三、七次调参,每一次都为什么

1. flash attention:从开到关(本次部署最重要的决定)

默认开启时看着很完美:33/33 层全上卡、100% GPU、占 3.4 GB、生成 8.6 tok/s。

但实测发现长提示会静默输出垃圾(一整串 0),而且稳定复现:

提示 token输出
1633正常
1708垃圾(全 0)
1783垃圾
1858垃圾(连测两次均复现)

关掉 FA 后,同样 1858 token 的提示完全正常。根因是 Vulkan 后端在 Polaris 上的 flash attention 在长序列存在数值缺陷,不是模型损坏、也不是参数配错,调参救不回来。

代价照实说:

FA 开FA 关(当前)
上卡层数33/331/33(16384 上下文时 0/33)
占用3.4 GB8.7 → 9.6 GB
生成速度8.6 tok/s5.5 tok/s
预填充55 tok/s72~74 tok/s(8192 时)
长提示 >1650 token输出垃圾正常

取舍逻辑只有一句:可预期的慢,优于不可预期的错。 给程序用的 API,正文突然变成一串 0 且不报错,比慢危险得多。

2. num_ctx:8192 → 16384

最初按"够用就好"定 8192,后来要接 Dify 这类会累积对话历史的多平台,才提到 16384。同样 4098 token 的提示,代价如下:

配置预填充每轮固定开销占用上卡
81925556 s(7274 tok/s)约 56 s8.7 GB1/33 层
16384(当前)约 76 s(54 tok/s)约 76 s9.6 GB0/33 层

每轮多花约 20 秒,原因是 KV 缓存翻倍 + 显卡装不下层,退化成接近纯 CPU 推理。

但要分清场景:WorkBuddy 每轮提示固定 4098 token 且不带历史,4098 + 2048 = 6146 本来就在 8192 之内。只给它用,8192 更划算;多平台共用才需要 16384。

3. num_predict:1024 → 2048

第一次接 WorkBuddy 就踩到:回答生成到 1024 token 正好撞上限被截断,客户端自动发起第二次请求续写,两段合计 7 分 38 秒;续写时模型还编造了一段"Token 溢出恢复模式"的自述文字。

WorkBuddy 长回答被截断后客户端二次续写,总耗时 7 分 38 秒

提到 2048 后该现象消失,代价是长回答最坏情况耗时翻倍。

4. OLLAMA_HOST:127.0.0.1 → 0.0.0.0

默认只监听本机,另一台电脑的 WorkBuddy 根本连不上。改成 0.0.0.0 后监听变为 :::11434,局域网访问 /api/version 实测返回 0.40.14。

安全提醒:这等于同网段任何设备都能免鉴权调用本机显卡。只建议在可信网络开;要收紧就只绑具体网卡 IP 并限定来源 IP。

5. keep_alive=-1 与 max_loaded=1

这台机器就是用来跑模型的,所以让模型永久占住显存(ps 里 UNTIL 显示 Forever),避免每次请求重新加载(首次加载约 20 秒)。同时限死只加载一个模型,防止 8 GB 显存被第二个模型撑爆。

6. NUM_PARALLEL=2:留着但没收益

实测 2 路并发:单路 8.7 s,两路分别 8.3 s / 16.6 s,总墙钟 16.6 s,相对串行 17.4 s 只有 1.05x。单路请求已经吃满显存带宽,加并发数不能提吞吐,只能排队。

更关键的是:上下文提到 16384 后,日志显示 n_slots = 1,Ollama 自己就把并发槽压回 1 了。

7. 别名固化参数

把上下文、采样、输出上限、系统提示全钉进 qwen-api 的 Modelfile,好处是任何客户端接进来都自动继承正确参数,不用每个平台各配一遍。

四、接第三方 Agent 客户端才知道的事

以 WorkBuddy 为例,配置很简单:

WorkBuddy 添加模型的填写方式

Base URL 填 http://本机IP:11434/v1(必须带 /v1),Key 随便非空即可,模型名必须与 ollama list 一致,思考模式不勾选。

接通之后暴露出来的三个事实,比模型本身更值得记:

  1. 每轮提示固定 4098 token,且每轮从头重算。 日志连续四次请求都是 4098,一个 token 不差——因为它不带对话历史,缓存无从命中。这就是"问什么都得先等一分钟"的原因,与问题长短无关。顺便说,这也是它记不住上一轮聊了什么的原因,不是模型健忘。
  2. 关掉"工具调用"并不能缩短提示。 取消勾选后仍是 4098,大头是客户端自己注入的项目记忆与系统提示。
  3. 一轮对话的真实耗时构成:提示 4098 token(预填充 56 s)+ 生成 544 token(97 s)= 服务端 3 分钟,界面显示 3 分 54 秒,多出来的是它自己启动运行环境的时间。

实测截图(一句话解释缓存,生成 544 token、正文完整、无思考残留):

WorkBuddy 完整回答示例,耗时 3 分 54 秒

简单算术题也验证过准确性与耗时(2+3*4 返回 14,9 的三次方返回 729):

两道小题的实测结果与耗时

五、另外两条坑

5.1 思考模式:两个接口两种关法,服务端关不掉

Qwen3.5 默认开思考,对程序调用是灾难——一句话回答烧掉 500+ token,正文可能为空。

接口关闭写法
原生 /api/chat、/api/generate"think": false
OpenAI 兼容 /v1/chat/completions"reasoning_effort": "none"(think:false 在此无效)

实测(同一问题,上限 150 token):

传参耗时token正文
什么都不传26.2 s150(烧满)空
think: false(走 /v1)25.9 s150空(参数被忽略)
reasoning_effort: "none"6.2 s3465 字正常

服务端试过三种固化方式,全部无效:Modelfile 里 PARAMETER think false(直接报 unknown parameter)、系统提示词强令禁止思考、提示词加 /no_think。结论:思考只能在调用方关。

例外是 WorkBuddy:界面里"思考模式"不勾选时输出是干净正文,说明它内部已经处理了。

5.2 1650 token 的红线

FA 关闭是当前配置,这条红线在 FA 开时才会触发;当前配置下长提示正常。但一旦有人为了提速把 FA 打开,就必须同步约束每次输入不超过 1500 token(约 1000 汉字),否则静默输出垃圾。

六、维护与排查

& "$env:LOCALAPPDATA\Programs\Ollama\ollama.exe" ps
& "$env:LOCALAPPDATA\Programs\Ollama\ollama.exe" list
Invoke-RestMethod -Uri "http://127.0.0.1:11434/api/version"

改参数:改 Modelfile → create qwen-api -f Modelfile → 下次请求自动生效。

看日志定位问题:server.log,重点看 prompt eval time(每轮真实提示长度与速度)、offloaded N/M layers(上卡层数)、flash_attn(FA 状态)。

现象先查什么
回答为空思考模式没关(/v1 用 reasoning_effort: none)
输出一整串 0提示过长且 FA 被打开了(>1650 token)
慢到几分钟预填充在 CPU 上跑,看 ps 的 PROCESSOR 占比
别的电脑连不上OLLAMA_HOST 没生效或防火墙拦了
改了环境变量没反应没用启动脚本,服务继承了旧环境

回滚:停进程 → unins000.exe /VERYSILENT → 删 .ollama 目录 → 清 6 个 OLLAMA_* 变量。

七、这套配置适合谁

适合:单机、少量客户端、能接受"一分钟级"响应、要求结果正确不能出垃圾的场景。

不适合:高并发、亚秒级响应、需要显卡吃满速率的场景。这代硬件(Polaris + Ivy Bridge)的天花板就在这里,软件层面能调的已经调完——再提速只剩换 N 卡走 CUDA,或换 RDNA3 走 ROCm 两条硬件路。