为什么专家缓存救不了稠密模型: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 层 MoE | 64 层(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 以下。
因此,稠密模型在低显存下的策略必须调整:
- 避免按 Token 动态搬运权重:哪些权重驻留 GPU、哪些留在 CPU,在会话启动前即行确定。
- 控制显存占用,消除冗余副本:16GB 显存容纳
13.26GiB权重余量有限,计入 KV Cache、循环状态、工作区和桌面占用后,对齐浪费或反量化副本易引发 OOM 或驱动降速。 - 跨设备仅传输激活值,保持全量原位压缩计算:显卡足以容纳大部分权重时,CUDA 算子直接处理 GGUF 原始压缩块;显存不足时,仅在单矩阵合法行边界进行切分,跨 PCIe 传输数十 KB 的激活向量。
第一步:核算显存预算,避免隐式降级与分页
在通用框架下运行大模型时,常出现类似现象:16GB 显存加载 14GB 模型,程序未崩溃,但生成速度从几十 tok/s 降至一两 tok/s。这通常由于超出物理可用显存,触发显卡驱动的共享显存分页(Shared GPU Memory Paging),系统在底层通过 PCIe 交换显存页面。
为避免此类掉速,我们在引擎前端加入了容量规划器(dense-plan)。在分配 GPU 显存前,通过如下公式核算准入预算:1
2
3
4
5
6
7Peak 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 B | 64 层主干 + Embedding/Output,排除第 65 层 MTP sidecar |
| CUDA 设备 Arena 分配字节 | 14,241,855,488 B | 单个连续显存池统一管理 |
| 整网显存对齐损耗 | 6,144 B (6KiB) | 相对 14.24GB 原始权重,对齐开销极低 |
| 匿名主机权重拷贝字节 | 0 B | backend_explicit_anonymous_host_weight_copy_bytes = 0 |
| 上传并裁剪后的进程 RSS | 110,560 KiB (~108MiB) | 释放已上传 GGUF 文件页缓存后的常驻内存 |
| 上传并裁剪后的进程 PSS | 107,022 KiB (~104.5MiB) | 其中映射文件页 Pss_File 仅剩 12,562 KiB |
该加载流程的处理方式如下:
- 排除第 65 层 MTP 权重:官方
Qwen3.8-27BGGUF 在 64 层主干后常附带第 65 层(blk.64)的多 Token 预测(MTP)权重。单 Token 解码模式下,静态图规划器验证blk.64描述符后在上传阶段将其排除,降低显存占用(如blk.64.ffn_down.weight的Q6_K矩阵占用73.1MB)。 - 只读
mmap绑定与分页裁剪:引擎通过只读mmap打开 GGUF 文件,描述符校验、形状核对与上传复用同一映射句柄。张量通过批量写入 CUDA 设备 Arena;上传提交后,对映射区间执行页面释放(Trim),将进程文件缓存回落至110MB附近。 - 空间不足时的单矩阵输出行切分(Output-Row Split):在显存更紧凑的设备上(如可用显存约
13.5GB),张量放置器无需全层退回 CPU,支持在合规矩阵(如ffn_gate或ffn_up)上按量化块对齐行切分:前缀行写入 GPU Arena,后缀行直接引用只读 GGUFmmap视图,由校验器检查精确覆盖(Exact Cover)。
第二步:免展开 FP16,CUDA 直接计算 12 种 GGUF 混合量化块
将 14.24GB 原始权重装入 16GB 显存后,核心在于:如何在 GPU 上直接利用压缩字节完成矩阵乘(GEMV)?
若在算子执行前将 4bit 权重解压为 FP16 临时矩阵,显存容量与带宽需求会显著上升。同时,真实 GGUF 模型的各层往往混用不同格式。
扫描该 Qwen3.8-27B IQ4 GGUF 的张量清单可见,为保持关键层精度,主干网络内混用了 12 种 GGUF 量化存储格式:1
2IQ4_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)的矩阵向量乘。主要处理方式包括:
- **GPU 端激活值实时量化为
Q8_F32S**:执行矩阵乘前,利用轻量核函数将 32 长度的 FP32 激活量化为带符号int8,并附带 FP32 缩放因子(中间格式Q8_F32S)。激活无需回传 CPU 预量化,后续内积直接调用 GPU 硬件的dp4a(4 元素int8点积累加)指令。 - 微型码本常驻与寄存器内解包:
- 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。
- K-quant 系列(
RTX 5060 Ti 16GB 上的真实矩阵 GEMV 微基准
我们在 RTX 5060 Ti 16GB 上提取 Qwen3.8-27B 真实权重层测试了这组原位 GEMV(预热后取 1000 次中位数,并与 CPU 参考实现逐行核对误差):
| 真实模型张量 | 量化格式 | 矩阵形状 (M × K) | 原始压缩体积 | Host 预量化延迟 | GPU 实时量化延迟 | 有效压缩权重带宽 | 相对 CPU 最大绝对误差 |
|---|---|---|---|---|---|---|---|
blk.7.ffn_gate.weight | IQ4_XS | 17408 × 5120 | 48.27MiB | 0.1335ms | 0.1364ms | 329.90GiB/s | < 1e-7 |
blk.58.ffn_gate.weight | Q5_K | 17408 × 5120 | 61.48MiB | 0.1494ms | 0.1526ms | 375.15GiB/s | < 1e-7 |
blk.13.ffn_gate.weight | Q3_K | 17408 × 5120 | 36.52MiB | 0.1904ms | 0.1944ms | 183.46GiB/s | 2.98e-8 |
blk.14.ffn_gate.weight | IQ2_S | 17408 × 5120 | 27.22MiB | 0.1269ms | 0.1300ms | 204.47GiB/s | 1.49e-8 |
blk.15.ffn_down.weight | IQ2_S | 5120 × 17408 | 27.22MiB | 0.1132ms | 0.1165ms | 228.24GiB/s | 3.35e-8 |
blk.23.attn_v.weight | Q6_K | 1024 × 5120 | 4.10MiB | 0.0151ms | 0.0176ms | 227.00GiB/s | 5.96e-8 |
blk.64.ffn_down.weight | Q6_K | 5120 × 17408 | 69.73MiB | 0.2000ms | 0.2043ms | 333.30GiB/s | 5.96e-8 |
实测表现体现出两项细节:
- GPU 实时量化激活开销有限:对比 Host 预量化,GPU 端实时将 FP32 激活量化为
Q8_F32S再做 GEMV,大矩阵耗时增加约0.003ms(约2.16%),计算结果逐位一致(差值为0)。层间传递 FP32 隐藏状态可在 GPU 内直接衔接。 - 高密度块格式的读取效率:在
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.13FFN 子层(三块压缩权重共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 合并分子与分母。
- Stage 1(Partial Chunks):按每
跨块合并使用 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) | 实测加速比 |
|---|---|---|---|
128 | 0.496ms | 0.464ms | 1.07x |
1,024 | 1.316ms | 1.275ms | 1.03x |
2,048 | 2.243ms | 1.304ms | 1.72x |
8,192 | 7.810ms | 1.372ms | 5.69x |
32,768 | 29.954ms | 4.040ms | 7.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 输出路径。
当前进度边界与待验证项
讨论性能工程,需区分已跑通的单算子微基准与尚未完成的整网端到端指标。
截至目前,引擎已验证落地的模块包括:
- GGUF 解析、分词与显存规划器:包含 HF 兼容的 Unicode 9 NFC 与 Qwen3.5 分词规则、官方聊天模板渲染,以及带精确覆盖校验的 GPU/Host 零拷贝张量放置。
Qwen3.8-27B12 种量化格式原位 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 测试机连通后补齐实机计时)。- 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 和显存水位的完整对比。