3090 上的部署两种基于Qwen3.5-4B 判别模型open jev:llama.cpp和 vLLM ,谁更快、谁更准 2026 年 9 月我把同一个 4B 模型用两种方式放到 RTX 3090 24GB 上对外都走 OpenAI 兼容接口做同一件事读完一篇舆情只回答一个字母。A 表示文中能看出具体毕业生和他的毕业院校B 表示看不出来。两边都是 4 bit上下文都是 8192思考模式都关掉。差别在运行时和量化落点。测完之后的结论很短要吞吐用 vLLM AWQ。220440 token 的判别请求单条延迟大约是 llama.cpp 的五分之一8 路并发吞吐高 35 倍。这张卡上还同时跑着 OCR。要少把「在读生」放进下一轮用 llama.cpp GGUF。18 条标注里它 18/18vLLM 16/18。错的两条都是边界句明确的校友新闻和纯企业新闻两边一致。量化档次差得不多。大矩阵都是 4 bit、组大小 32。GGUF 把一部分敏感矩阵留在 56 bitAWQ 把线性注意力的输入投影和 lm_head 留在原精度。下面是部署、踩坑和测试记录。1. 这个模型到底在判什么SemIf-OpenJev 不是一套单独的微调权重。官方基线就是冻结的Qwen/Qwen3.5-4B。所谓 SemIf是一次前向、只读选项字母 logits 的决策协议。量化之后的概率不会和 BF16 逐位相同项目 README 里的准确率也是 BF16 上的数字不能直接套到 Q4 上。我用的协议如下。系统提示固定为Apply the supplied criterion to the supplied evidence. Choose exactly one listed option. Respond with only its uppercase letter, with no explanation or reasoning.用户消息是一段 JSON{evidence:标题……\n正文……,criterion:这篇舆情是否包含高校并且能看出毕业生及其毕业院校,options:[{letter:A,description:文中有高校且能看出具体毕业生是谁以及其毕业院校。},{letter:B,description:没有高校或看不出具体毕业生或看不出其毕业院校。仅有企业、合作或泛称校友也不够。}]}请求参数参数值原因max_tokens1只要首字母temperature1.0跟 SemIf 直接打分的设置对齐真正的决策看 logprob不看采样logprobs/top_logprobstrue / 8取出 A、B 的对数概率chat_template_kwargs.enable_thinkingfalseQwen3.5 默认会思考思考 token 会把 1 个字母的请求拖成一段推理决策时对 A、B 的 logprob 做 softmax。缺字母就当保留。生产上的丢弃规则更严只有判成 B且 B 的概率 ≥ 0.85才丢掉。接口超时、报错、置信度不够都保留交给后面的大模型。彩票、招生简章不走这个模型标题正则在更前面就过滤掉。输入必须是标题加全文。毕业院校经常写在正文后半段只看标题会误杀。2. 两张卡的约束不一样机器是多张 RTX 3090 24GB。两种部署占的卡不同比较速度时要带着这个前提。方案 A方案 B运行时llama.cppllama-servervLLM 0.28vllm serve权重bartowskiQwen3.5-4B-Q4_K_MGGUFcyankiwi/Qwen3.5-4B-AWQ-4bit卡GPU1整卡独占GPU4和已有 OCR 进程分卡端口80918092对外模型名Qwen3.5-4B-SemIf进程内是Qwen3.5-4B-AWQ网关再映射成 SemIf上下文每槽 8192max-model-len 8192镜像vllm/vllm-openai:v0.28.0-cu129同一份镜像镜像名字容易看错。两个容器用的是同一份 vLLM 镜像GPU1 那个容器把入口换成了自己挂进去的llama-server所以界面上的镜像名一样里面跑的不是 vLLM。3. 方案 AGPU1 上的 llama.cppGGUF 来自bartowski/Qwen_Qwen3.5-4B-GGUF文件Qwen_Qwen3.5-4B-Q4_K_M.gguf约 3.01 GB。/v1/models回报词表 248320嵌入 2560参数量约 43.3 亿量化标记Q4_K - Medium训练上下文 262144。服务时把每条请求限制在 8192。二进制是事先编好的llama-server需要 glibc 2.38宿主机跑不了只能放进容器。镜像只是提供这个 glibc 和 CUDA入口被--entrypoint换掉。3.1 槽位怎么算llama.cpp 里-c是整池上下文再被并行槽数切开。-c 8192 -np 64不会给每条请求 8192日志里每槽只剩几百 token。正确写法是统一 KV 池-np 56 --kv-unified --kv-unified-per-slot 8192启动日志sizing KV pool to n_parallel * kv_unified_per_slot 56 * 8192 458752 n_slots 56, n_ctx_slot 8192, kv_unified true458752 n_ctx_train这条警告可以忽略。那是池子的总 token 数单条请求仍然被帽在 8192。3.2 为什么是 56 而不是 64Qwen3.5-4B 有 32 层每 4 层才有一层完整注意力也就是 8 层真正占 KV。KV 头 4 个head_dim 256FP168 层 × 2(KV) × 4 头 × 256 × 2 字节 32 KB/token 8192 token × 32 KB ≈ 256 MB/槽64 槽的 KV 大约 16 GB加上约 3 GB 权重再加ubatch2048的计算图约 2.5 GB24 GB 放不下启动时还差大约 2.5 GB。ubatch4096的计算图更大大约 4.7 GB更不可能。56 槽、ubatch2048能稳住显存大约 22.4 GB / 24 GB。实际启动参数llama-server\-mQwen_Qwen3.5-4B-Q4_K_M.gguf\--host0.0.0.0--port8091\-np56--kv-unified --kv-unified-per-slot8192\-b8192-ub2048-ngl99--flash-attn on\-t8\--aliasQwen3.5-4B-SemIf--metrics容器侧还要加上--gpus device1、--ipchost、--network、--cpus8、--restart unless-stopped以及把 GGUF 和二进制只读挂进去。llama.cpp 这一侧没有 API Key。短提示、8 路、预热之后16 到 56 槽的预填充吞吐差不多都在每秒 24002800 token 附近。槽位加到 56 不是为了把短请求的 token/s 再抬一截而是为了让 56 篇 8192 以内的文章可以同时占着 KV不被切成 256 token 的残槽。4. 方案 BGPU4 上的 vLLM AWQ权重是 cyankiwi/Qwen3.5-4B-AWQ-4bitrevisionef85d23。config.json里的量化方法是compressed-tensors版本0.14.1.dev20格式pack-quantized。这不是旧的 AutoAWQ 格式。不要加--quantization awqvLLM 0.28 会按 compressed-tensors 自己识别。量化配置目标是Linear权重量化激活不量化4 bit对称 int组大小 32observer 是mse不量化的有 148 个张量视觉塔 98 个线性注意力in_proj_a/in_proj_b共 48 个再加上lm_head和 MTP 的fc服务时加了--language-model-only视觉塔不进推理但它仍然占着「不量化」名单里的那部分权重体积。4.1 和 OCR 分卡时利用率不能按空闲显存来填vLLM 的gpu_memory_utilization乘的是整卡显存并且启动时要求空闲显存不少于这个乘积。GPU4 上 OCR 已经占了大约 12.9 GB空闲大约 11.5 GB。0.9会去要 21 GB 以上启动直接失败。0.42 × 24 GB ≈ 10.1 GB低于当时的空闲可以启动。启动之后的日志Using FLASH_ATTN attention backend out of potential backends: FLASH_ATTN, FLASHINFER, TRITON_ATTN, FLEX_ATTENTION Model loading took 3.28 GiB Available KV cache memory: 4.4 GiB GPU KV cache size: 101,282 tokens Maximum concurrency for 8,192 tokens per request: 12.36x3090 是 SM 8.6不是 Hopper也不是 SM100。这条日志里的优先级就是实测选中的顺序FlashAttention 2 已经是这张卡上的第一选择。32 层里只有 8 层走这个后端另外 24 层是线性注意力。GDN 的 decode 在这个镜像里回退到了 Triton因为对应的 fused CUDA kernel 没有编进来。这不影响正确性只是 decode 不是最快的那条路径。判别请求几乎只有 prefill 加 1 个 token这条回退影响很小。首次启动要给 Dynamo 编译留时间这次大约 34 分钟。编译缓存写在容器可写层里docker rm -f再拉会重新编译。当前正在跑的进程只注册了Qwen3.5-4B-AWQ。启动脚本后来改成了两个--served-model-nameQwen3.5-4B-SemIf和Qwen3.5-4B-AWQ但没有重建容器所以上游名字还是 AWQ。网关做名字映射即可不必为了改名重启。vllm serve cyankiwi/Qwen3.5-4B-AWQ-4bit\--served-model-name Qwen3.5-4B-SemIf\--served-model-name Qwen3.5-4B-AWQ\--host0.0.0.0--port8092\--max-model-len8192\--gpu-memory-utilization0.42\--language-model-only\--reasoning-parser qwen3\--default-chat-template-kwargs{enable_thinking: false}\--trust-remote-codeHF 缓存只读挂进容器并设置HF_HUB_OFFLINE1避免启动时再去拉仓库。5. 量化差在哪两边文件都读过张量类型不是看仓库名字猜的。GGUF 共 441 个张量类型个数主要落在FP32232LayerNorm、大部分 SSM 小参数Q4_K124FFN gate/up、注意力 gate以及一半 FFN downQ6_K49词嵌入、线性注意力的 QKV、另一半 FFN down、一部分全注意力 Q/VQ5_K16全注意力的 K 和 outputQ8_020少量 SSM以及 MTP / 末层的个别矩阵Q4_K 是超块 256、组大小 32每组有 scale 也有 min不是纯对称量化。Q4_K_M 的「M」就是这张混精度表敏感矩阵升到 5 bit、6 bit 甚至 8 bit其余大矩阵留在 4 bit。AWQ 这一侧更整齐该量化的 Linear 全部是对称 int4、组大小 32线性注意力的in_proj、lm_head、视觉塔保持原精度。所以文件大约 3.8 GB比 GGUF 的 3.0 GB 大运行时权重约 3.28 GiB。对应关系可以收成三句话FFN 的 gate/up两边都是 4 bit。线性注意力的大投影AWQ 留原精度GGUF 压到 Q6_K。AWQ 在这块更满。全注意力的 QKV/OGGUF 多用 56 bitAWQ 用 4 bit。GGUF 在这块更满。没有出现「一边 2 bit、一边 8 bit」这种档次差。对只输出一个字母的判别来说这个差距会表现成边界句上的分歧而不是明显样本被判反。官方 BF16 的分数两边都对不齐互相之间也不会逐位相同。6. 对外合成一个模型名业务侧只认Qwen3.5-4B-SemIf。两个后端都挂到 New API 上通道一http://semif-qwen35-4b:8091模型名就是Qwen3.5-4B-SemIf通道二http://qwen35-4b-awq-gpu4:8092上游模型名Qwen3.5-4B-AWQmodel_mapping把Qwen3.5-4B-SemIf改写成Qwen3.5-4B-AWQ改完通道、能力和模型倍率之后要重启网关否则内存里的模型表不会出现新名字。没配模型倍率时网关会直接 400提示价格未配置。两个通道的权重都设成 0 时网关会把同名请求分到两边。做对比测试时不要走网关直接打 8091 和 8092。思考开关也可以在网关的param_override里再写一次chat_template_kwargs.enable_thinkingfalse避免某个客户端漏传。7. 测试怎么做的才公平三件事如果不做数字会骗人。先把网关上的两条通道暂时禁用再直接打容器端口。否则线上请求和测试请求挤在同一批槽位里llama.cpp 会被拖到几十秒一条。测完再打开通道。请求体必须是上面的 SemIf JSON。用一段普通续写去压测模型会输出 C、E、N 之类的字母那个吞吐不能当成判别速度。重复同一条长文会命中 prompt cache。我第一次把同一篇 732 token 的文章打 8 遍llama.cpp 的单条延迟从 0.42 秒掉到 0.13 秒。后面改成每条提示都旋转正文、前缀不同才作为速度结论。准确率用了 18 条我自己标的短句。判 A 必须同时看见「是谁」和「毕业于哪所高校」原文和每一条的理由写在第 9 节。样本不大其中一部分短句自己把结论写在了句尾。8. 速度去缓存之后的结果规模llama.cpp / GPU1vLLM AWQ / GPU4约 220 token单条中位0.41 秒0.07 秒约 220 token8 路5.2 条/秒约 1150 token/秒24.8 条/秒约 5500 token/秒约 440 token单条中位0.48 秒0.09 秒约 440 token8 路5.6 条/秒约 2500 token/秒15.8 条/秒约 6900 token/秒另外一组 16 条互不重复的短句约 175 tokenllama.cppvLLM单条中位0.42 秒0.07 秒16 条串行总时间7.6 秒1.3 秒vLLM 单条大约快 5 倍。8 路并发下短请求大约快 5 倍440 token 大约快 3 倍。GPU1 有 56 个槽GPU4 满上下文理论并发只有 12 左右但判别请求远短于 8192vLLM 的连续批处理把这 8 条叠在一起56 个槽并没有换成更高的短请求吞吐。更长的全文一两千 token 以上接近 8192这次没有单独拉满。220 和 440 两档的差距方向一致。如果生产里每篇都顶满 8192GPU4 的 KV 只有 4.4 GiB并发会被 12 这条线卡住GPU1 的 56×8192 池子更大。那种负载要另测不能把上面的 5 倍直接外推到满上下文。9. 准确率先把判分规则说死。一条文章要标A留下必须同时看见两件事具体是谁这个人毕业于哪所高校少一条就是B丢掉。在读生、只写在高校上班、只说「多名校友」但没有人名、只有高中、只有校名没有人都是 B。模型输出的是 A 或 B 的概率。线上还有第二道闸判成 B 并且 B 的概率不到 0.85仍然留下。所以「模型选了哪个字母」和「线上会不会丢掉」不是一回事。18 条里只看字母谁的概率更高llama.cpp 18/18vLLM 16/18。两边错的方向都是该丢的没丢没有把写明毕业院校的人误删。下面按原文列出来。9.1 应该留下原文写明了人和毕业院校这 5 条两边都判 A置信度都 ≥ 0.85线上都会留下。高拯。原文「高拯云霄一中 1956 届高中毕业生。1961 年毕业于上海同济大学建筑工程系毕业后留校任教。」人是高拯高校是同济大学动词是「毕业于」。前面的高中不改变结论。刘欢写了毕业。原文「1985 年刘欢刚从国际关系学院毕业随后到宁夏支教。1991 年入职对外经济贸易大学任教三十余年。」人是刘欢毕业院校是国际关系学院。后面的对外经济贸易大学是任职不是这条要找的毕业信息但毕业那一句已经够了。小米创始团队。原文「雷军小米创始人毕业于武汉大学计算机科学系。林斌毕业于中山大学无线电电子工程系后获美国德雷克塞尔大学硕士。卢伟冰毕业于清华大学化学系。」三个人、三所高校都写了「毕业于」。撒贝宁、李健、张鲁一。原文「撒贝宁在节目中自述保送北京大学法学院。李健毕业于清华大学电子工程系。张鲁一有中央戏剧学院本科和北京大学艺术硕士学历。」三个人都能对上学校。保送北大法学院、本科、艺术硕士都算能看出毕业或就读并完成院校。陈七。原文「陈七是浙江大学电气工程学院 1998 级校友现就职于宁德时代研发部。」有人名、有学院、有年级。「校友」在这里指向这个人的毕业院校不是空泛的「多名校友」。9.2 应该丢掉而且两边都丢掉了这 10 条两边都判 B置信度都 ≥ 0.85线上都会丢掉。送进模型的原文为什么是 B中建八局西南公司 2027 届校园招聘启动面向应届本科、硕士毕业生以及博士研究生。公司总部位于成都未点名任何毕业生及其毕业院校。只有「应届毕业生」这个统称没有某一个人也没有他的学校船舶电子电气工程毕业生可以入职中船集团。文章只谈专业去向没有写出任何具体毕业生姓名和毕业院校。只有专业名没有人南通大学人工智能研究院主办国庆一日 AI 科技研学营面向小学到高中学生结营颁发研学证书。文中没有高校毕业生。南通大学是主办方学员是中小学生行程包括剑桥大学卡文迪许实验室。郭毅可任英国帝国理工学院数据科学研究所所长。马云出席开幕。没有写谁毕业于这些学校。有高校、有人名但写的是职务和出席不是毕业电影《小小的我》故事落点是脑瘫青年刘春和收到师范大学录取通知书。这是入学不是毕业。录取通知书是即将入学9 月 26 日热门股雷科防务、康强电子、平潭发展、新华传媒。全文是股票名单没有高校也没有毕业生。没有高校也没有人68 岁的周老太太每天通勤 14 小时帮儿子带娃退休金三千全部倒贴。全文没有高校也没有毕业生。没有高校也没有毕业生北京大学与华为技术有限公司签署战略合作协议共同建设联合实验室。张三代表华为出席文中没有他的毕业院校。有高校、有人名张三的学校没写多位清华校友创办了这家公司但文章没有写出任何一位校友的姓名。有清华没有具体的人刘欢在对外经济贸易大学任教三十年开设西方音乐史职称副教授。文章没有写他毕业于哪所学校。有人、有高校但这是任职。毕业院校没出现这 10 条有一个测试上的缺陷不少句子的后半句已经把结论写出来了例如「未点名任何毕业生」「这是入学不是毕业」。模型等于看见了提示。两边都判对只能说明它们读得懂这种短句不能当成对长新闻的考试。9.3 三条例外这三条正文没有把「请判 B」写在脸上也是两个模型会分开、或者线上不会照字母执行的地方。1. 张展硕还在读书。两边判反了。原文「张展硕不是体育特招的刻板印象。他是复旦大学新闻学院 2025 级本科生去年接受央视采访时说学新闻是为了让更多人了解游泳。」人是张展硕学校是复旦大学新闻学院但身份是2025 级本科生文章没有写他已经毕业。规则要求的是毕业生所以人标 B。字母概率按 0.85 阈值线上会不会丢掉llama.cppB0.95会丢掉vLLM AWQA0.98不会。它把在读生当成了毕业生而且很确信这是 18 条里唯一一条干净的分歧。vLLM 会把这篇放进下一轮抽取。2. 只写了「就学」没写「毕业」。字母不同线上结果相同。原文「刘欢及长就学京师国际关系学院专修法文。其后半生执鞭杏坛任教对外经贸大学三十余载。」能看出刘欢在国际关系学院读过书也能看出他后来在对外经贸大学教书。正文没有「毕业」两个字。我按严口径标了 B没写毕业就不算看出毕业院校。这个标注偏严人读也会犹豫。字母概率线上会不会丢掉llama.cppB约 0.85刚到丢弃线会丢掉vLLM AWQA0.56不会。0.56 远低于 0.85就算它判了 A规则也是留下3. 只有高中。字母相同但都不够确信线上都留下。原文「高拯是云霄一中 1956 届高中毕业生后来长期在建设部工作。文中没有出现高校。」有人名有「毕业生」但学校是中学不是高校。人标 B。字母B 的概率线上会不会丢掉llama.cppB0.56不会vLLM AWQB0.78不会两个模型都看出了「更像该丢」但置信度都没过 0.85。这不是谁判反是阈值把犹豫样本留了下来。9.4 这 18 条能说明什么写明「某人毕业于某高校」的 5 条两边都留下。带有答案提示的 10 条短句两边都丢掉。真正拉开差距的是张展硕那一条llama.cpp 把在读本科生挡在外面vLLM 以 0.98 的概率放了进去。10. 踩坑清单这些都是这次部署里实际撞上的。-c和-np是整池再均分。要每条 8192用--kv-unified --kv-unified-per-slot。64 槽在 24GB 上会 OOM。先减 ubatch再减槽数。56 和 ubatch 2048 是这张卡的实测上限。gpu_memory_utilization乘总显存不乘空闲显存。卡上已有进程时先算空闲 / 总显存再留一点余量。这份 AWQ 不是 AutoAWQ。不要传--quantization awq。Qwen3.5 不关思考max_tokens1没有意义。服务端默认模板和请求体里都要enable_thinking: false。同一个 vLLM 镜像可以只当运行环境。入口换成 llama.cpp 之后界面上的镜像名仍然是 vLLM。看进程和/v1/models的owned_by不要看镜像仓库前缀。网关要配模型倍率改完要重启。否则新模型名要么 400要么根本不在/v1/models里。压测必须绕开网关并且每条提示的前缀要不同。否则测到的是负载均衡、排队和 prompt cache。11. 怎么选需求选择同样的卡还要留给别的服务又要更高的短文本吞吐vLLM AWQ把利用率压到空闲显存允许的比例整卡可用希望同时挂住更多篇 8192 上下文llama.cpp56 槽的统一 KV 池判别规则卡得很死在读生、未写明毕业的不能进下一轮llama.cpp。这次样本里它没有把在读生判成毕业生明确校友 / 明确非校友的粗过滤后面还有大模型兜底两边都能用。0.85 的阈值本身就会把犹豫样本留下来我现在的接法是两个都挂在同一个模型名后面。对比和回归打到端口上线上如果更在意速度就把 AWQ 那条通道的权重调高。阈值继续放在 0.85调用失败仍然保留。12. 这次结论的边界速度是在禁用线上流量之后用互不重复的 220 和 440 token 提示测的8 路并发各做了预热。准确率是 18 条人工短句5 条写明了毕业院校10 条句尾带了答案提示真正拉开差距的是「在读本科生」那一条。「就学」那条标得偏严。更大的标注集上边界分歧可能会多几条但「写明毕业院校时两边一致、vLLM 明显更快」这两点以这次的重复测量看方向是稳的。