为什么专家缓存救不了稠密模型:16GB 显存跑 27B Dense 模型的推理优化实践

前两篇聊了怎么在 8GB 和 6GB 显存上用 Strata(原名 FreeToken)运行 Qwen3.6-35B-A3B。文章发出后,常有读者问起:

既然 35B 模型能靠内存和显存协同跑起来,Qwen3.8-27B 参数量更小,能不能照搬这套专家缓存方案?

这个问题在 Strata 的 GitHub 仓库里也有人提过(Niko1221/Strata#770)。作者当时回复得明确:Qwen3.8-27B 属于稠密(Dense)架构,需要另一套独立的推理引擎。

单看文件体积,Qwen3.6-35B-A3B 和 Qwen3.8-27B 的 4bit 量化包都在 14GB 到 18GB 之间。进入逐 Token 解码阶段后,两者的访存行为截然不同。最近我围绕 Qwen3.8-27B 的 IQ4 混合量化 GGUF 模型,写了一套面向单卡 6GB、8GB、16GB 显存的 C++20/CUDA 稠密推理引擎(位于仓库内 tools/dense-inference-engine),并在 RTX 5060 Ti 16GB(Blackwell sm_120)上完成了核心算子、显存放置和单子层的实机验证。

这里结合这套引擎的工程数据,把稠密模型在低显存下的推理优化拆开讲清楚:为什么 MoE 的缓存思路在此处行不通、16GB 显存如何规划空间、CUDA 如何直接处理 12 种 GGUF 混合量化块,以及单流执行与分块注意力如何降低延迟。

为什么专家缓存救不了稠密模型

先回顾 Strata 在 Qwen3.6-35B-A3B 上跑通的前提。

Qwen3.6-35B-A3B 是稀疏 MoE 模型:每层有 256 个路由专家,生成单个 Token 只激活其中 8 个路由专家和 1 个共享专家。整份模型权重虽有 35B,单步前向计算实际只读取约 3B 参数。在连续生成上下文时,相邻 Token 往往反复命中同一批专家。显卡仅需常驻非专家层和大约 512 个专家槽位;遇到未命中的专家,再由 CPU 协同计算或走 PCIe 异步换入。

到了 Qwen3.8-27B,结构完全不同:

维度Qwen3.6-35B-A3B (MoE)Qwen3.8-27B (Hybrid-Dense)
主干层数40 层 MoE64 层(48 层 Gated DeltaNet + 16 层 Full Attention)
FFN 结构256 选 8 稀疏路由专家 + 1 共享专家每层均为完整稠密 FFN(H=5120, I=17408)
单 Token 激活参数约 3B(占总参数约 8.5%)全部 27B 主干参数(100% 参与计算)
跨 Token 权重局部性强(存在明显的热点专家)无(每个 Token 都要扫过每一层的全部矩阵)

Qwen3.8-27B 的 64 层主干采用混合序列混合器(48 层线性注意力 Gated DeltaNet 搭配 16 层全注意力 Full Attention),每一层的投影矩阵和 FFN(ffn_gate、ffn_up、ffn_down)都是稠密矩阵。生成任何一个 Token,都必须将 64 层的权重完整计算一遍。

可以算一笔带宽账。以这份 Qwen3.8-27B 的 IQ4 GGUF 文件为例,主干 64 层的原始张量总字节数是 14,241,849,344 字节(约 13.26GiB)。如果机器只有 8GB 显存,剩余约 6GiB 权重留在主机内存,每个 Token 都通过 PCIe 4.0 x16(实测有效带宽通常在 25GB/s 至 28GB/s)向 GPU 搬运一次,单是传输这 6GiB 权重就需花费 200 毫秒以上,解码速度会被压制在 4 tok/s 以下。

因此,稠密模型在低显存下的策略必须调整:

  1. 避免按 Token 动态搬运权重:哪些权重驻留 GPU、哪些留在 CPU,在会话启动前即行确定。
  2. 控制显存占用,消除冗余副本:16GB 显存容纳 13.26GiB 权重余量有限,计入 KV Cache、循环状态、工作区和桌面占用后,对齐浪费或反量化副本易引发 OOM 或驱动降速。
  3. 跨设备仅传输激活值,保持全量原位压缩计算:显卡足以容纳大部分权重时,CUDA 算子直接处理 GGUF 原始压缩块;显存不足时,仅在单矩阵合法行边界进行切分,跨 PCIe 传输数十 KB 的激活向量。

第一步:核算显存预算,避免隐式降级与分页

在通用框架下运行大模型时,常出现类似现象:16GB 显存加载 14GB 模型,程序未崩溃,但生成速度从几十 tok/s 降至一两 tok/s。这通常由于超出物理可用显存,触发显卡驱动的共享显存分页(Shared GPU Memory Paging),系统在底层通过 PCIe 交换显存页面。

为避免此类掉速,我们在引擎前端加入了容量规划器(dense-plan)。在分配 GPU 显存前,通过如下公式核算准入预算:

1
2
3
4
5
6
7
Peak VRAM = GPU 驻留权重 (GPU weights)
+ 全注意力 KV Cache (FP16 KV cache)
+ 循环层状态 (Gated DeltaNet recurrent state)
+ 持久工作区 (Persistent workspace)
+ max(Prefill 临时区, Decode 临时区)
+ 传输暂存区 (Staging buffers)
+ 安全余量 (Safety reserve)

核算规则包含三点:

  • 以真实可用显存(Available VRAM)为准,不依赖显卡标称容量:桌面环境、浏览器与 CUDA 上下文会消耗显存。若设定上下文长度超出可用容量,直接返回明确错误与拆解账单,不自动截断上下文或将 FP16 KV 降级为低精度。
  • 拆分核算 Full Attention KV 与 Gated DeltaNet 循环状态:Qwen3.8-27B 仅有 16 层全注意力层(kv_heads=4, head_dim=256),FP16 精度下单层每 Token 占用 4,096 字节,整网 16 层每 Token 需 65,536 字节(64KiB/token)。8K 上下文对应 KV Cache 占用 512MiB,32K 上下文对应 2GiB。
  • 循环状态使用固定大小双缓冲(Active/Staging Double Bank):其余 48 层 Gated DeltaNet 不产生随长度增长的 KV Cache,但需维护卷积历史(conv_kernel=4)与矩阵状态(value_heads=48, state=128)。单层逻辑状态为 3,268,608 字节(约 3.12MiB),48 层合计每序列 156,893,184 字节(约 149.6MiB)。为防止执行中途出现非有限值(NaN/Inf)或 OOM 污染现有状态,显存内维持 active 与 staging 两组缓冲,仅当整步核函数执行完毕且 4 字节状态码为 0 时,才切换 active 指针。

实测:14.24GB 权重的零拷贝放置与 6KB 对齐损耗

明确容量上限后,需将 GGUF 中的多张量放置入 GPU 显存,同时避免主机内存膨胀。

部分加载方案在读取模型时,将文件读入匿名主机内存后再做重排或解压,随后调用 cudaMemcpy 写入显卡,易导致主机内存(RAM)峰值翻倍。

我们在 dense-cuda-load-probe 中实现了原始张量打包与放置流程,在 RTX 5060 Ti 16GB(实测空闲显存约 15.6GiB)上完成了对完整 Qwen3.8-27B GGUF 的实机验证:

指标RTX 5060 Ti 16GB 实测数值说明
主干原始张量字节数14,241,849,344 B64 层主干 + Embedding/Output,排除第 65 层 MTP sidecar
CUDA 设备 Arena 分配字节14,241,855,488 B单个连续显存池统一管理
整网显存对齐损耗6,144 B (6KiB)相对 14.24GB 原始权重,对齐开销极低
匿名主机权重拷贝字节0 Bbackend_explicit_anonymous_host_weight_copy_bytes = 0
上传并裁剪后的进程 RSS110,560 KiB (~108MiB)释放已上传 GGUF 文件页缓存后的常驻内存
上传并裁剪后的进程 PSS107,022 KiB (~104.5MiB)其中映射文件页 Pss_File 仅剩 12,562 KiB

该加载流程的处理方式如下:

  1. 排除第 65 层 MTP 权重:官方 Qwen3.8-27B GGUF 在 64 层主干后常附带第 65 层(blk.64)的多 Token 预测(MTP)权重。单 Token 解码模式下,静态图规划器验证 blk.64 描述符后在上传阶段将其排除,降低显存占用(如 blk.64.ffn_down.weight 的 Q6_K 矩阵占用 73.1MB)。
  2. 只读 mmap 绑定与分页裁剪:引擎通过只读 mmap 打开 GGUF 文件,描述符校验、形状核对与上传复用同一映射句柄。张量通过批量写入 CUDA 设备 Arena;上传提交后,对映射区间执行页面释放(Trim),将进程文件缓存回落至 110MB 附近。
  3. 空间不足时的单矩阵输出行切分(Output-Row Split):在显存更紧凑的设备上(如可用显存约 13.5GB),张量放置器无需全层退回 CPU,支持在合规矩阵(如 ffn_gate 或 ffn_up)上按量化块对齐行切分:前缀行写入 GPU Arena,后缀行直接引用只读 GGUF mmap 视图,由校验器检查精确覆盖(Exact Cover)。

第二步:免展开 FP16,CUDA 直接计算 12 种 GGUF 混合量化块

将 14.24GB 原始权重装入 16GB 显存后,核心在于:如何在 GPU 上直接利用压缩字节完成矩阵乘(GEMV)?

若在算子执行前将 4bit 权重解压为 FP16 临时矩阵,显存容量与带宽需求会显著上升。同时,真实 GGUF 模型的各层往往混用不同格式。

扫描该 Qwen3.8-27B IQ4 GGUF 的张量清单可见,为保持关键层精度,主干网络内混用了 12 种 GGUF 量化存储格式:

1
2
IQ4_XS, IQ4_NL, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K,
IQ2_XS, IQ2_S, IQ3_S, IQ3_XXS, Q8_0

这 12 种格式占主干张量字节数的 **99.92496%**(14,231,162,880 / 14,241,849,344 字节),其余 0.075% 为 360 个 FP32 归一化与偏置向量(共 10,686,464 字节)。格式分布包含:

  • 常规 FFN 层采用 IQ4_XS、Q4_K 或 Q3_K;
  • blk.0.ffn_gate.weight 与 blk.12.ffn_gate.weight 采用高压缩比的 IQ2_XS(共 51.5MB);
  • blk.14 至 blk.17 部分 FFN 矩阵采用 IQ2_S 与 IQ3_XXS;
  • blk.3.attn_q.weight 与 blk.15.attn_q.weight 分别采用 IQ4_NL 和 Q2_K;
  • blk.23.attn_v.weight 与 blk.63.attn_v.weight 采用 6bit 的 Q6_K;
  • 词表嵌入表 token_embd.weight(546,304,000 字节)采用 Q3_K。

缺少其中任一格式均会导致相应层无法执行。我们在 CUDA/PTX 中实现了这 12 种格式的原位(In-place)GEMV 核函数,以及 Q3_K 嵌入表的按需单行 GPU 解码。

Q8_F32S 中间激活与 DP4A 寄存器内解码

Decode 阶段每次输入的激活向量为 1 个 Token(维度 K = 5120 或 17408),属于访存受限(Memory-bound)的矩阵向量乘。主要处理方式包括:

  1. **GPU 端激活值实时量化为 Q8_F32S**:执行矩阵乘前,利用轻量核函数将 32 长度的 FP32 激活量化为带符号 int8,并附带 FP32 缩放因子(中间格式 Q8_F32S)。激活无需回传 CPU 预量化,后续内积直接调用 GPU 硬件的 dp4a(4 元素 int8 点积累加)指令。
  2. 微型码本常驻与寄存器内解包:
    • K-quant 系列(Q2_K、Q3_K、Q4_K、Q5_K、Q6_K):核函数直接按 256 元素原始 GGUF 块读取高低位与子块缩放因子;Q2_K 与 Q5_K 在寄存器中同步计算 sum(q8) 修正项;Q6_K 块按字节组装避免非对齐读取。
    • I-quant 系列(IQ2_XS、IQ2_S、IQ3_XXS、IQ4_XS、IQ4_NL):不生成整矩阵反量化表,在芯片上维护固定 1KiB 或 2KiB 的非线性网格码本(Codebook),线程束读取 74/82/98 字节压缩块后查表拼装 4 字节整数送入 dp4a。

RTX 5060 Ti 16GB 上的真实矩阵 GEMV 微基准

我们在 RTX 5060 Ti 16GB 上提取 Qwen3.8-27B 真实权重层测试了这组原位 GEMV(预热后取 1000 次中位数,并与 CPU 参考实现逐行核对误差):

真实模型张量量化格式矩阵形状 (M × K)原始压缩体积Host 预量化延迟GPU 实时量化延迟有效压缩权重带宽相对 CPU 最大绝对误差
blk.7.ffn_gate.weightIQ4_XS17408 × 512048.27MiB0.1335ms0.1364ms329.90GiB/s< 1e-7
blk.58.ffn_gate.weightQ5_K17408 × 512061.48MiB0.1494ms0.1526ms375.15GiB/s< 1e-7
blk.13.ffn_gate.weightQ3_K17408 × 512036.52MiB0.1904ms0.1944ms183.46GiB/s2.98e-8
blk.14.ffn_gate.weightIQ2_S17408 × 512027.22MiB0.1269ms0.1300ms204.47GiB/s1.49e-8
blk.15.ffn_down.weightIQ2_S5120 × 1740827.22MiB0.1132ms0.1165ms228.24GiB/s3.35e-8
blk.23.attn_v.weightQ6_K1024 × 51204.10MiB0.0151ms0.0176ms227.00GiB/s5.96e-8
blk.64.ffn_down.weightQ6_K5120 × 1740869.73MiB0.2000ms0.2043ms333.30GiB/s5.96e-8

实测表现体现出两项细节:

  1. GPU 实时量化激活开销有限:对比 Host 预量化,GPU 端实时将 FP32 激活量化为 Q8_F32S 再做 GEMV,大矩阵耗时增加约 0.003ms(约 2.16%),计算结果逐位一致(差值为 0)。层间传递 FP32 隐藏状态可在 GPU 内直接衔接。
  2. 高密度块格式的读取效率:在 IQ4_XS、Q5_K 和 Q6_K 矩阵上,按压缩体积折算的有效权重带宽达到 330GiB/s 至 375GiB/s。

此外,针对 546.3MB 的 Q3_K 词表嵌入矩阵(token_embd.weight),引擎保持压缩整表常驻,根据输入的 Token ID 在 GPU 定位并解码单行 5120 维向量,单次查找(含同步与校验)耗时 0.1018ms,相对 CPU 参考实现误差为 0。

第三步:单流子层融合、工作区复用与分块长上下文注意力

矩阵计算优化后,若每算完一个矩阵均执行 cudaStreamSynchronize,或每层频繁分配临时显存,64 层累加的调度开销依然可观。

我们在子层与整网执行器上做了三项结构调整:

1. FFN 子层单流串联与 165KB 工作区复用

Qwen3.8-27B 的稠密 FFN 子层计算链如下:

1
RMSNorm -> Q8_F32S 激活量化 -> gate/up 两次 GEMV -> SiLU(gate) * up -> 第二次 Q8_F32S 量化 -> down GEMV + 残差相加

整串操作全部排入单条非阻塞 CUDA Stream,中间无主机端往返。连续同形状层(H=5120, I=17408)复用同一块 165,504 字节(约 161.6KiB)的临时工作区。

在 RTX 5060 Ti 16GB 上实测:

  • 全 Q3_K 的 blk.13 FFN 子层(三块压缩权重共 114.9MB),将“每层同步”改为“批次末尾统一同步”后,单子层耗时从 **0.663ms 降至 0.596ms**(降幅约 10.1%)。
  • blk.14 至 blk.17 连续四层混合量化 FFN(gate/up 为 IQ2_S,down 为 IQ3_XXS),单子层平均耗时从 **0.518ms 降至 0.463ms**(降幅约 10.7%)。

Gated DeltaNet 循环层采用相同机制。以 blk.5 为例,将 RMSNorm -> 四路 GEMV -> 因果深度卷积 -> 逐头 L2 归一化 -> 状态衰减与 Delta 更新 -> Gated RMSNorm -> 输出投影 与紧随其后的 FFN 串入同一 Stream,整个 recurrent + FFN Block 单步执行中位数为 **0.972ms/block**,与 CPU 参考实现的余弦相似度为 0.99997。

2. 长上下文全注意力:两阶段分块归约

Qwen3.8-27B 包含 16 层全注意力层(如 blk.11、blk.23、blk.63,配置 query_heads=24, kv_heads=4, head_dim=256)。上下文达到 8K 或 32K 时,单 CTA 顺序扫描历史 Token 容易导致流多处理器(SM)并行度不足,延迟随长度上升。

为缓解长上下文 Decode 的注意力开销,我们采用自适应两阶段(Two-stage Chunked)归约:

  • 短上下文(总 Token 数 <= 1024):执行单阶段在线 Softmax,每个 Query Head 一个 CTA,基础工作区占用 132,736 字节(129.6KiB)。
  • 长上下文(总 Token 数 > 1024):
    • Stage 1(Partial Chunks):按每 1024 个历史 Token 切分分块,每个 (chunk, query_head) 组合启动独立 CTA,计算局部点积,输出 FP32 局部最大值、FP32 分子向量与 FP64 分母。
    • Stage 2(Cross-Chunk Reduce):每个 Query Head 启动归约 CTA,用全局最大值重标定分块权重,通过 FP64 合并分子与分母。

跨块合并使用 FP64 双精度累加分母,以维持长序列的数值稳定性。32K 上下文下单层注意力总临时工作区为 928,384 字节(约 0.89MiB),FP16 KV Cache 保持每层每 Token 4,096 字节。

在 RTX 5060 Ti 16GB 上,对 blk.11 全注意力子层进行单阶段(warp3)与分块(warp4)的实测对比(21 轮中位数):

上下文总 Token 数旧单阶段耗时 (ms/layer)新两阶段分块耗时 (ms/layer)实测加速比
1280.496ms0.464ms1.07x
1,0241.316ms1.275ms1.03x
2,0482.243ms1.304ms1.72x
8,1927.810ms1.372ms5.69x
32,76829.954ms4.040ms7.41x

在 blk.23 和 blk.63(含 Q6_K 投影)复测,8K 上下文单层耗时为 **1.35ms / 1.34ms**,32K 上下文为 **4.00ms**,历史深度 2048 下余弦相似度均高于 0.99997。

3. Prompt 前缀的 State-Only 执行路径

处理 Prompt 输入时,若输入序列长为 N,前 N-1 个 Token 仅需更新各层 KV Cache 与 Gated DeltaNet 循环状态,无需获取中间预测结果。

若前 N-1 个 Token 仍执行完整输出头,每步需额外运行 5120 维末尾 RMSNorm、248,320 × 5120 词表投影 GEMV,以及将近 1MB(248,320 × 4 字节)的 Logits 回传(D2H)。

单流执行器(qwen35_cuda_token_graph_executor)提供了 state-only 模式:Prompt 前 N-1 个 Token 在第 64 层隐藏状态、KV 与循环状态校验通过后直接提交,跳过词表投影与 Logits 拷贝;仅第 N 个 Prompt Token 和后续生成的 Token 走完整 full 输出路径。

当前进度边界与待验证项

讨论性能工程,需区分已跑通的单算子微基准与尚未完成的整网端到端指标。

截至目前,引擎已验证落地的模块包括:

  1. GGUF 解析、分词与显存规划器:包含 HF 兼容的 Unicode 9 NFC 与 Qwen3.5 分词规则、官方聊天模板渲染,以及带精确覆盖校验的 GPU/Host 零拷贝张量放置。
  2. Qwen3.8-27B 12 种量化格式原位 CUDA GEMV:主干 14.24GB 权重全部 12 种压缩块均可在 GPU 原位计算,无需展开 FP16 副本;已通过 RTX 5060 Ti 16GB 上的单矩阵、FFN 子层、blk.5 循环块与 blk.11/23/63 全注意力层的数值与基准测试(新增的 IQ4_NL、Q2_K、IQ2_XS 三种格式已通过本机静态图准入与 CPU 参考验证,待 RTX 5060 Ti 测试机连通后补齐实机计时)。
  3. 64 层单流 Token 执行器骨架:将 Embedding、48 层 Recurrent、16 层 Full Attention、64 层 FFN、双缓冲循环状态、FP16 KV 与 state-only 前缀跳过整合至单条 CUDA Stream 和单块共享可变 Arena。

同时,以下项目仍在推进中,**不能由单层耗时累加推导整网 tok/s**:

  • 真实 64 层端到端 GPU 吞吐(tok/s)与首字延迟(TTFT)实测:单流 64 层执行器已通过编译与合成 4 层设备测试,但 64 层 27B 真实模型的实机端到端速度、显存峰值与逐层 Logits 差分仍待在测试机完成。
  • Prefill 阶段的批量 GEMM 与双缓冲流式预取:目前 state-only 路径省去了词表头计算,底层仍为逐 Token 调度核函数,后续需通过 Batched GEMM 摊薄长 Prompt 读取开销。
  • 与 llama.cpp / LM Studio 的同条件端到端 A/B 测试:需在相同 GGUF SHA256、相同 Token ID 序列、相同 FP16 KV 精度和相同上下文深度下完成对比。

小结:本地运行大模型,依架构选择策略

结合前两篇介绍的 Strata 与本次的稠密引擎,本地部署的选型逻辑如下:

  • 使用 6GB 或 8GB 显存 显卡运行 30B 级别模型,**Qwen3.6-35B-A3B 这类稀疏 MoE 模型搭配 Strata** 是可行方案。单 Token 仅激活 3B 参数,借助专家缓存和 CPU-GPU 协同可绕开显存容量限制。
  • 运行 Qwen3.8-27B 等稠密模型,每一步必须计算完整权重,按步换入权重的缓存机制受限于 PCIe 带宽。16GB 显存是更切合实际的起步配置,优化重心在于显存预算核算、GGUF 混合量化块的原位寄存器解码、单流算子融合以及长上下文注意力的分块归约。

待 64 层端到端 GPU 吞吐与 Prefill 批量优化取得实测数据后,再行整理整网 tok/s、TTFT 和显存水位的完整对比。