想把 DeepSeek-V4.1-Flash 这类大模型放在自己机房里跑,最现实的问题就是显存不够。本文记录一台 4 卡 4090 机器上的实测结果:模型能跑起来,长上下文也能吃下,但速度只适合内部低频使用,不适合对外提供高并发服务。
如果你正准备做类似尝试,可以直接参考本文的 docker-compose.yml 与调优结论。
先说结论
| 项目 | 实测结果 |
|---|---|
| 硬件 | 4× RTX 4090 48GB,单卡 49140 MiB,共约 192GB 显存 |
| 模型 | DeepSeek-V4.1-Flash-1017,GGUF UD-IQ3_S 量化,8 个分片 |
| 推理后端 | llama.cpp server-cuda 官方镜像 |
| 启动耗时 | 容器启动到模型加载完成约 11 秒(mmap + lazy load) |
| 显存占用 | 四卡共约 187GB,单卡 45~48GB |
| Prefill 速度 | 稳定后约 100 tokens/s |
| Decode 速度 | 约 10~12 tokens/s |
| 最大上下文 | 65536 tokens(单 slot) |
| 适用场景 | 内部低频问答、长文档分析;不适合多并发线上服务 |
一句话:能跑,长上下文可用,但速度属于"能等"的级别。 65000 tokens 的 prompt 需要约 11 分钟才能完成预填充。
硬件与系统环境
| 项目 | 配置 |
|---|---|
| GPU | NVIDIA GeForce RTX 4090 × 4,驱动 595.99.02,CUDA 13.2 |
| 单卡显存 | 49140 MiB(48GB 版本,非公版 24GB) |
| 内存 | 125 GiB,Swap 8 GiB |
| 操作系统用户 | app@aiwork1,模型与 compose 文件放在 ~/ai/docker_m/deepseek-v41-flash-1017 |
重要前提:这里用的是 48GB 显存的 4090(涡轮/魔改版本)。普通 24GB 4090 四卡只有 96GB 显存,无法用本文这套配置放下整个模型,必须改用更激进的量化或更多 CPU 卸载。
四张卡的拓扑从 nvidia-smi 看是两两一组(81/82 与 C1/C2 两个 PCIe 域),跨组通信会慢一些,这也是后面 --tensor-split "1,1,1,1" 均分仍能接受的原因——瓶颈主要不在卡间带宽,而在 CPU 侧卸载的张量。
软件环境
使用官方 llama.cpp CUDA 镜像,省去自己编译 CUDA 后端:
image: ghcr.io/ggml-org/llama.cpp:server-cuda
模型目录以只读方式挂载:
volumes:
- /home/app/ai/models/DeepSeek-V4.1-Flash-1017-GGUF:/models:ro
模型选择:为什么用 UD-IQ3_S
模型文件为 UD-IQ3_S(Unsloth Dynamic 3-bit 动态量化)的 8 分片 GGUF:
/models/UD-IQ3_S/DeepSeek-V4.1-Flash-1017-UD-IQ3_S-00001-of-00008.gguf
选择理由很直接:在 192GB 显存下,只有 3-bit 级别的量化才可能把权重基本放进 GPU,同时给 64K 上下文的 KV Cache 留出空间。UD-IQ3_S 属于动态量化,对敏感层保留更高精度,实际效果通常好于普通均匀 IQ3。
如果显存更紧张,可以退到 IQ2 系列;如果追求质量,则需要 6 卡以上或换用更大显存的卡。
完整 docker-compose.yml
这是本机实际在用的配置,可以直接复制修改:
services:
deepseek-v41-flash-1017:
image: ghcr.io/ggml-org/llama.cpp:server-cuda
container_name: deepseek-v41-flash-1017
restart: no
ports:
- "8813:8813"
volumes:
- /home/app/ai/models/DeepSeek-V4.1-Flash-1017-GGUF:/models:ro
gpus: all
shm_size: "16gb"
ulimits:
memlock: -1
stack: 67108864
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,utility
command:
- --model
- /models/UD-IQ3_S/DeepSeek-V4.1-Flash-1017-UD-IQ3_S-00001-of-00008.gguf
- --host
- 0.0.0.0
- --port
- "8813"
# 多 GPU
- --split-mode
- layer
- --tensor-split
- "1,1,1,1"
- --n-gpu-layers
- "999"
# 上下文先保持小
- --ctx-size
- "65536"
- --parallel
- "1"
- --fit
- off
# Engram + 强制更多大张量到 CPU(关键)
- --load-mode
- mmap
- --lazy-mode
- on
- --override-tensor
- "per_layer_token_embd.weight=CPU,engram.*=CPU,blk\\.(0|1|2|3|11|12|15|25|26|27|31|35)\\.ffn_(down|gate|up)_exps\\.weight=CPU"
# Flash Attention + 更省的 KV
- --flash-attn
- on
- --cache-type-k
- q8_0
- --cache-type-v
- q8_0
# 采样
- --temp
- "1.0"
- --top-p
- "1.0"
- --top-k
- "0"
- --min-p
- "0.0"
# 模板
- --jinja
- --reasoning
- on
- --reasoning-format
- deepseek
- --chat-template-kwargs
- '{"reasoning_effort":75}'
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
启动与查看日志:
docker compose up -d
docker logs -f deepseek-v41-flash-1017
关键参数说明
1. 卸载策略:--override-tensor 是核心
这套配置真正的关键不在多卡切分,而在这里:
--override-tensor "per_layer_token_embd.weight=CPU,engram.*=CPU,blk\\.(0|1|2|3|11|12|15|25|26|27|31|35)\\.ffn_(down|gate|up)_exps\\.weight=CPU"
per_layer_token_embd.weight=CPU:逐层 token embedding 体量大、访问稀疏,放 CPU 省显存代价最小。engram.*=CPU:Engram 相关张量整体放 CPU。blk\.(...)\.ffn_(down|gate|up)_exps\.weight=CPU:把指定层(03、11、12、15、2527、31、35)的 MoE 专家 FFN 权重压到 CPU。
因为 DeepSeek 是 MoE 架构,专家权重占大头。挑出这些层交给 CPU,可以把每卡显存从"不够"降到"刚好够",代价是 decode 时要通过 PCIe 读取部分权重。
注意 YAML 里正则的反斜杠写成 \\.,最终传给程序的才是 \.。
2. --load-mode mmap + --lazy-mode on
mmap:权重按需从页缓存映射,启动只需 11 秒,不必把上百 GB 权重全部读进内存。lazy-mode on:延迟加载,模型"加载完成"并不等于权重已就位,前几个请求会明显变慢(见下文实测)。
代价是首次访问会触发磁盘/页缓存读取,并且依赖系统内存做缓存。本机 free -h 显示 buff/cache 高达 93Gi、free 只剩 933Mi,说明页缓存确实被模型文件占满了。
3. --flash-attn on + KV Cache 量化
--flash-attn on --cache-type-k q8_0 --cache-type-v q8_0
65536 tokens 的 KV Cache 在 FP16 下非常吃显存,改成 q8_0 大约省一半,是能把上下文开到 64K 的前提。实测精度损失在可接受范围内。
4. --split-mode layer --tensor-split "1,1,1,1"
按层切分到四张卡,四卡等分。切分后仍出现一条警告:
resolve_fused_ops: layer 11 is assigned to device CUDA1 but fused DeepSeek V4 HC pre is assigned to device CUDA0
resolve_fused_ops: fused DeepSeek V4 HC pre not supported, set to disabled
说明融合算子(fused DeepSeek V4 HC pre)在当前版本还不支持跨设备,被自动禁用。这属于正常降级,不影响正确性,但会损失一点性能。
5. 采样与推理
--temp 1.0 --top-p 1.0 --top-k 0 --min-p 0.0
按官方推荐放开采样。对话模板侧使用 --jinja --reasoning on --reasoning-format deepseek,并通过 reasoning_effort: 75 控制思考强度——这个值越高,思考 token 越多,decode 时间越长,本地部署时建议适当降低。
--parallel 1 意味着单 slot 串行处理。192GB 显存下没有余量做多并发,强行提高只会 OOM。
实测性能
以下数据来自同一次会话的 slot print_timing 日志,机器空闲、无其他负载。
短对话
| 场景 | prompt | prefill | decode |
|---|---|---|---|
| 首轮请求(冷启动) | 42 tokens | 12.72 s / 3.30 tok/s | 30.13 s / 9.92 tok/s |
| 第二轮(复用前缀,仅 10 个新 token) | 10 tokens | 0.60 s / 16.65 tok/s | 25.55 s / 11.70 tok/s |
首轮 42 个 token 花了 12.7 秒,是典型冷启动开销(图初始化 + lazy 加载)。第二轮命中 LCP 前缀缓存(f_sim_best = 0.757),几乎瞬间完成 prefill,decode 提升到 11.7 tok/s。
长上下文(65K)
| 指标 | 数值 |
|---|---|
| prompt tokens | 65012 |
| prefill 总耗时 | 659.59 s(约 11 分钟) |
| prefill 速度 | 98.56 tok/s(稳定在 98~103 tok/s) |
| decode(300 tokens) | 24.58 s / 12.16 tok/s |
| 总耗时 | 684.17 s |
prefill 速度曲线很平稳:起步 92 tok/s,在 4096 tokens 附近爬到约 103 tok/s,之后随上下文增长缓慢回落到 98 tok/s。说明长上下文的经济性主要取决于总 token 数,而不是衰减。
结论很明确:65K 上下文的首字延迟是分钟级的。适合"提交长文档,等几分钟拿结果"的场景,不适合交互式对话。
显存与内存占用
nvidia-smi 中四卡占用(空闲态,模型常驻):
| GPU | 显存占用 | 温度 | 功耗 |
|---|---|---|---|
| 0 | 45368 MiB / 49140 MiB | 36°C | 29W |
| 1 | 46816 MiB / 49140 MiB | 32°C | 32W |
| 2 | 47758 MiB / 49140 MiB | 35°C | 20W |
| 3 | 47208 MiB / 49140 MiB | 36°C | 34W |
四卡合计约 187GB,单卡余量只剩 1.43.8GB。推理时 GPU 利用率会打满,但空闲功耗只有 2034W(P8 状态),长期挂着成本可接受。
主机内存:
total used free shared buff/cache available
Mem: 125Gi 32Gi 933Mi 204Mi 93Gi 92Gi
Swap: 8.0Gi 5.9Gi 2.1Gi
有两点值得注意:
buff/cache93Gi 基本都是模型文件的页缓存,free只剩 933Mi 属于正常现象,available仍有 92Gi。- Swap 已用 5.9Gi,说明运行过程中出现过内存压力。如果遇到偶发卡顿,优先怀疑换页。
踩过的坑与调优建议
1. mmap 与 CPU 卸载同时使用会掉性能
启动日志中的警告值得重视:
tensor overrides to CPU are used with mmap enabled - consider using --load-mode none for better performance
当前这样配是为了快速启动 + 低内存占用。如果机器有足够内存、且不介意启动慢(需要把权重完整读入),改成 --load-mode none 能减少运行时的 mmap 缺页开销,decode 速度会有改善。两种模式是"启动快"和"跑得快"的取舍。
2. 没有任何鉴权,且 CORS 全开
security: no API key is set and CORS allows all origins
这是默认行为,不能直接暴露到公网。至少要做到:
- 只监听内网地址,前面加 Nginx 反代并做鉴权;
- 或使用
--api-key设置密钥。
3. 环境变量与命令行参数冲突
warn: LLAMA_ARG_HOST environment variable is set, but will be overwritten by command line argument --host
镜像自带 LLAMA_ARG_HOST / LLAMA_ARG_PORT,被 command 里的 --host / --port 覆盖,只是提示,不影响运行。想彻底消除可以在 compose 里把这些环境变量删掉。
4. slot 会长时间保留长上下文
日志里可以看到,处理完 65K 的请求后,slot 仍然持有 65338 个 token,后续请求按 LRU 选中它。在 --parallel 1 下这意味着下一个请求会继承旧上下文的显存占用,甚至因前缀相似度低而重新 prefill 一大段。如果不需要长会话记忆,定期重启容器或降低 --ctx-size 更稳妥。
5. 显存余量极小
单卡只剩 1~4GB 余量,任何参数上调都可能 OOM:
- 想加并发:先降
--ctx-size,再考虑--parallel 2; - 想开更大上下文:把更多层写进
--override-tensor的 CPU 列表; - 想提质量换更高量化:显存不够,只能加卡。
适合谁用
适合:内部知识库长文档分析、离线批处理、个人/小团队的低频调用、需要数据完全不出内网的场景。
不适合:面向用户的在线对话服务(decode 10~12 tok/s、单并发)、需要低首字延迟的 RAG 交互、高 QPS 场景。
如果你的需求是后者,同样四张 4090 更适合跑 30B 级别的稠密模型或更小的 MoE,速度会好一个数量级。跑 V4.1-Flash 这种体量,本质上是"用时间换显存"。
附:快速验证
服务起来后可以这样验证:
curl http://127.0.0.1:8813/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v41-flash-1017",
"messages": [{"role": "user", "content": "用一句话介绍你自己"}],
"max_tokens": 128
}'
同时观察 docker logs -f 里的 print_timing 行,tg 是 decode 速度,prompt eval time 是 prefill 耗时,是最直接的性能依据。