27B 模型塞进 24GB 显卡:SGLang + RedHatAI Qwen3.8-27B 的显存经济学

27B 模型塞进 24GB 显卡:SGLang + RedHatAI Qwen3.8-27B 的显存经济学

手里有两张 RTX 3090,一共 48GB 显存,想本地跑一个 27B 的现代大模型。这个目标听起来不算难——但真动手才知道,这条路每一步都在跟显存讨价还价:模型要量化到 4-bit,KV 缓存要精打细算,上下文要从 32K 一路抠到 156K,连”多模态预留的 1GB 显存”都要想办法抢回来。
这篇文章不是框架教程,是我在这条路上摸出来的完整账本:为什么选 SGLang 而不是 vLLM、为什么用 Red Hat 官方量化的 Qwen3.8-27B、每一个参数背后的代价是什么、最后压出多少性能。文末附所有模型的下载地址。

一、引擎:SGLang 靠什么赢下这场选型

本地推理引擎的主流选项就几个:llama.cpp、vLLM、TensorRT-LLM、SGLang。我们最终双卡全上了 SGLang(github.com/sgl-project/sglang),理由是它在一个我们最看重的指标上赢得很彻底——同一个模型、同一张 24GB 卡,SGLang 能装下的上下文是 vLLM 的三倍
这不是估计,是同模型实测。拿生产在用的 RedHatAI Qwen3.8-27B 分别部署到两套引擎:
速度方面,SGLang 46 t/s vs vLLM 39.4 t/s(500 token 长输出稳定均值,不是短输出噪声);上下文方面,SGLang 102K vs vLLM 32K。
差距的根源不在模型,在显存结构。vLLM 在 Ampere 架构(3090)上,fp8 KV 缓存只能用 FlashInfer 后端——这个组合既占显存又慢;SGLang 的 fp8 实现深度融合,同样的 24GB 能装下三倍的 KV 池。vLLM 不是不好,它的价值在格式兼容性(能跑 SGLang 不支持的 3-bit 等冷门量化),而不是替代 SGLang。
至于 llama.cpp,它的单请求速度确实快,但它是串行的——我们真正高频的场景是”一次派发 3 个子代理”,SGLang 的连续批处理能把 3 并发聚合到 130 t/s,llama.cpp 只能 73 t/s。并发,才是这个时代的刚需。
我们当前跑在 v0.5.18(2026-08-22 发布,710 个 PR、212 位贡献者)。这一版对我们最实在的更新:

更新 意义
torch 2.13 + triton 3.7.1 + FlashInfer 0.6.17 三大底层依赖换代,kernel 全面重写
GDN 内核系列优化 针对 Qwen3.8 混合 Gated DeltaNet 架构的专项改进,正对我们跑的模型
SGLANG_CACHE_DIR 统一缓存 各 kernel 缓存收敛到一处,升级后首次启动重编译一次
Rust Server 原生多模态 前端服务层逐步迁移到 Rust,Qwen VL 已原生支持

二、模型:为什么是 RedHatAI Qwen3.8-27B

Qwen3.8-27B 是 Qwen 官方开源的 27B 级模型,Apache-2.0 协议,原生多模态,底子很好。但 FP16 原始权重 54GB,24GB 卡根本放不进去——量化不是可选项,是入场券。

RedHatAI/Qwen3.8-27B-INT4 是 Red Hat 官方用 vLLM 团队的 LLM Compressor 工具量化的版本,我们拿它当生产主力跑了一个多月:

量化方案:AWQ + GPTQ 双算法(duo_scaling),W4A16,group 128,actorder 按权重排序。只量化 transformer 线性层:视觉塔、输出头保留原始 BF16。权重约 19GB:24GB 卡上给 KV 缓存留下约 5GB。零补丁:vLLM、SGLang 都能直接加载。

选它的核心逻辑是”官方 + 严谨”——Red Hat 的量化工程比社区第三方更值得信任,19GB 这个体积在 24GB 卡上刚好是甜点区。

不过这里有个必须说清楚的取舍。19GB 权重意味着上下文上限大概在 100K 附近。我们最终在 GPU1 上换成了 born2bewild 的激进量化版——它连 lm_head(INT4)和 embedding(INT8)一起压,整包 15.81GB,省下的 3.6GB 全部变成 KV 缓存,上下文冲到 156K。代价是 SGLang 原生不认识量化过的 embedding/lm_head,需要打三个补丁(已提 PR #36137)。

所以选型其实是条岔路:

要省心(零补丁、大厂量化、多模态):RedHatAI,回滚备用随时可用。要极限(24GB 单卡上最大上下文):born2bewild,当前生产。

三、调优:把 24GB 显存算到一分不差

部署 SGLang 的核心是理解一个参数:–mem-fraction-static。它定义”模型权重 + KV 缓存池”占 GPU 总显存的比例,是整个显存预算的起点。我们把 27B 从 0.92 一路试到 0.98:

mem-fraction KV 池 (tokens) CUDA Graph 捕获 推理
0.92 56,991
0.95 78,608 48.8 t/s,甜点
0.96 85,813 ❌ OOM
0.97 93,019 ❌ OOM
0.98 100,224

最有迷惑性的是 0.96/0.97:CUDA graph 捕获成功了,但推理时 OOM——activation 需要额外 256MB,这是捕获之外独立的一层限制。0.95 是”捕获成功 + 推理可用”的精确边界,也是 27B 在 24GB 上的最优解。

接下来是上下文的三轮扩展,全程没有多花一分显存:

阶段 改动 KV 池 上下文
初始 默认配置 66,640 65,536
no_buffer + cache9 + disable-overlap 81,006 80,000
+ mm-feature-transport=cpu 120,079 119,000

第 ② 步是”白捡”的:SGLang 默认给多模态池在显存里预留 1GB(CUDA IPC),改成 CPU 传输后这 1GB 直接归还 KV 预算。纯文本场景零影响——我们又不跑图,这 1GB 就是被闲置的财富。

当前生产配置(GPU1,156K 上下文):

sgl-qwen27b-gpu1:
  image: lmsysorg/sglang:latest          # v0.5.18
  container_name: sgl-qwen27b-gpu1
  ports: ["15433:30000"]
  environment:
    - CUDA_VISIBLE_DEVICES=1
    - SGLANG_MAX_NEW_TOKENS_LIMIT=32000  # 单请求输出上限
  command: >
    python3 -m sglang.launch_server
      --model-path /models/born2bewild-Qwen3.8-27B-W4A16-AutoRound-fast
      --port 30000 --host 0.0.0.0
      --dtype bfloat16
      --context-length 159744            # 156K = 156×1024
      --mem-fraction-static 0.92
      --max-total-tokens 159744          # KV池精确匹配context,释放prefill余量
      --max-mamba-cache-size 9           # 3并发 (no_buffer, 3 slots/req)
      --mamba-radix-cache-strategy no_buffer
      --disable-overlap-schedule
      --mm-feature-transport cpu         # 回收1GB多模态池
      --attention-backend flashinfer     # fp8 KV必须
      --mamba-ssm-dtype bfloat16         # GDN state减半
      --kv-cache-dtype fp8_e4m3
      --reasoning-parser qwen3
      --tool-call-parser qwen3_coder
      --sleep-on-idle

逐条讲清楚这几个参数背后的代价,因为它们没有一个是免费的:
--max-total-tokens 是显存预算书。 很多人不知道:KV 池大小由 mem-fraction 决定,跟 context 参数无关。context 调大但 --max-total-tokens 不匹配,KV 池会跟 prefill 临时空间抢显存,大输入直接 OOM。把它精确匹配到 context,等于告诉框架”KV 池就这么多,剩下的都给计算临时空间”。这是修复大输入崩溃的开关,也是整个调优里性价比最高的一行。
fp8 KV 缓存必须配 FlashInfer。 用 triton 后端会直接崩溃(fp8e4nv 不支持)。而 fp8 KV 让池容量比 bf16 翻倍,还更快——反直觉,但实测就是这样。
--max-mamba-cache-size 9 决定 3 并发。 Qwen3.8 是混合 Gated DeltaNet 架构,mamba state cache 是并发的硬约束:9 个 slot ÷ 每个请求 3 个 = 3 并发。日志里那句 max_running_requests is capped to 3 就是证据。想提并发,只能加大这个值或调 mamba ratio,代价是 KV 池缩小。
GDN 层必须用 bfloat16。 混合架构的 mamba 状态默认 float32,改成 bfloat16 后每 slot 减半,KV 池从 75K 直接涨到 165K——这是”白捡”的又一处,只是藏得比较深。

四、测试:数据不撒谎

并发聚合吞吐

并发 墙钟 总 tokens 聚合吞吐
1 4.3s 200 46.1 t/s
2 4.5s 400 88.9 t/s
3 4.5s 591 130.2 t/s

CUDA graph 捕获的 batch size 集合 [1,2,3] 恰好覆盖并发甜点,第 4 个并发会超出捕获范围而退化。

大上下文压力测试(v0.5.18 升级后)

输入规模 GPU0 (130K ctx) GPU1 (156K ctx) 用时
12K ~5s
22K ~19s
36K ~25s
72K ~44s

prefill 吞吐稳定在 1300-1600 token/s,全程无 OOM。这里踩了个隐蔽的坑:超过 50K token 的请求体用 curl -d ‘{…}’ 直接传会报”参数列表过长”——这是 ARG_MAX 限制,不是框架故障,改用 curl -d @file.json 就正常了。大上下文测试时,请求构造方式本身可能就是”假故障”来源。

升级踩坑(v0.5.18 特有)

一、–startup-weight-load-mode overlap 是陷阱:release notes 说启动提速 2.38 倍,但量化模型直接报 ValueError: not supported。release notes 的亮点数据往往是针对非量化大模型的,新参数先看官方源码确认支持范围。

二、torch 2.13 让显存重新洗牌:升级后空闲显存从 1.5GB 掉到 152MB——prefill CUDA graph 捕获从 ~0.9GB 涨到 1.17GB。空闲时看不出来,一跑大输入就 OOM。

三、补丁要基于新版本重打:我们提给上游的补丁(PR #36137)基于 v0.5.17,升级到 v0.5.18 后 qwen3_5.py 有官方改动,需要重打——好在实际只差一行 quant_config。

最终生产形态

GPU 端口 模型 context 职责
GPU0 11434 born2bewild 27B 133,120 开发项目直连 / 日常对话
GPU1 15433 born2bewild 27B 159,744 opencode 全部子代理

双卡同模型零漂移,各 3 并发互不打架;速度 47-51 t/s,质量对比官方量化版同题测试全部持平。

五、模型与框架下载地址

资源 地址
SGLang 推理框架 https://github.com/sgl-project/sglang
Qwen3.8-27B 原版 https://huggingface.co/Qwen/Qwen3.8-27B
RedHatAI Qwen3.8-27B-INT4 https://huggingface.co/RedHatAI/Qwen3.8-27B-INT4
born2bewild W4A16-fast(激进量化) https://huggingface.co/born2bewild/Qwen3.8-27B-W4A16-AutoRound-fast

六、这套账,值不值得

回头看,在 24GB 单卡上跑 27B 模型,全程只做一件事:把显存每一笔账算清楚。mem-fraction 卡在精确边界、KV 池用 –max-total-tokens 精确匹配、多模态池 1GB 回收、GDN 状态降精度——每一个数字背后都是实测,不是猜。

如果你也要做类似的部署,最值得记住的三条:

一、同模型对比才有意义。vLLM 慢不是模型的问题,是引擎的差距——换引擎前先拿同一个模型跑一遍。

二、新参数先看源码支持范围。release notes 的亮点数据面向的硬件和模型可能和你完全不同,量化模型尤其容易踩空。

三、显存预算透明化是一切的基础。把所有隐性占用都变成显式参数,你才有资格谈”榨干硬件”。

本地部署的乐趣不在于”我跑起来了”,而在于”我知道每一个数字为什么是这个值”。这张 24GB 的牌桌,算清了,就能赢。


皖ICP备2025105865号-2|皖公网安备34010402704739号