
最近一直在折腾我那两台老旧的 Tesla V100 16G 数据中心卡。手里正好拿到一组 QUASAR-NVFP4 量化模型权重——这是用 NVFP4 这个 4 位浮点格式压过的生成式大模型。问题是NVFP4 设计时针对的是 Blackwell 一代的硬件特性V100 这种 Volta 架构的老卡根本不认。但模型尺寸摆在那儿FP16 权重肯定塞不进两块卡的显存NVFP4 刚好能压到一半怎么看都想搏一把。最后是靠社区里那个代号 1Cat 的 vLLM 分支把这事跑通了。这篇文章就把我的折腾过程完整记录下来包括为什么 NVFP4 在 V100 上反而值得用、1Cat-vLLM 的加载原理、完整安装部署步骤、启动参数、实测性能和一路踩过的坑给同样只有旧卡但想跑大模型的朋友一个可复现的参考。1. 项目拆解与整体设计1.1 NVFP4 到底是什么为什么 V100 能跑先说点背景。NVFP4 是 NVIDIA 主推的一种 4 位浮点量化格式常见的布局是 1 位符号、2 位指数、1 位尾数也就是 E2M1总共 4 bit 能表达 16 种状态覆盖的数值范围挺宽但精度很粗。所以实际落地时不会裸用而是按 group 配一个缩放因子 scale比如每 32 个元素一组共用一个 FP32/BF16 的 scale靠“窄数值 高精度缩放”把动态范围撑住。这种设计思路跟 INT8/INT4 完全不一样。整数量化是把权重缩放到 [-127, 127] 的整数网格对分布敏感尾部大值会吃掉大量量化步长NVFP4 则是指数分布天然适配神经网络权重那种“大多数值集中在零附近、少量大值拖尾”的形态所以同是 4 bitNVFP4 在很多任务上的精度损失反而比 INT4 更可控。但硬件层面有个尴尬点NVFP4 的专用计算单元是新架构才有的V100 的 Tensor Core 只支持 FP16、INT8、INT4 这一类看到 NVFP4 数据等于看到乱码。硬要在老卡上跑就必须在计算前把 NVFP4 反量化成 FP16/BF16。这就回到了 1Cat-vLLM 这套方案的核心思路不要把整个模型一次性解包成 FP16 塞显存而是把 NVFP4 权重直接留在显存里只占 FP16 一半等真正做矩阵乘法的时候在 kernel 内部一块一块解出来、算完、释放临时 buffer。显存是被压缩的花掉的只是额外的解量化计算量听起来就划算。1.2 1Cat-vLLM 的定位与选型逻辑vLLM 官方主分支对模型格式的支持主要面向新硬件V100 跑 NVFP4 这件事官方大概率不接。1Cat 这个分支属于社区维护的“工程魔改版”目标很明确让老卡也能吃上新一代量化模型。它内部有一组自定义的融合算子专门处理 NVFP4 权重在 V100 上的“边反量化边计算”同时保留了 vLLM 原生的 PagedAttention、Continuous Batching、Tensor Parallel 这些能力所以不是简单把模型加载进 PyTorch 然后手动推理而是能直接以 OpenAI 兼容 API 的方式对外提供高吞吐服务。选型上我其实也对比过两条路一条是用 Transformers 自定义反量化 hook 自己推理实现非常自由但吞吐和并发调度要全部自己写V100 的显存本来就紧大概率并发一上来就 OOM另一条就是直接用 1Cat-vLLM等于把调度、显存管理、批处理这些脏活全交给框架。我最后选了后者事实证明这是对的省下了大量调优时间。1.3 硬件认知两块 V100 的底牌先交代一下我的实测环境显卡2× NVIDIA Tesla V100 16GSXM2 版本NVLink 桥接sm_75 计算能力CPU至强 Gold 6230内存 128G系统Ubuntu 22.04驱动 535.154.05CUDA12.2PyTorch 2.1.0V100 虽然是 2017 年的架构但 16G HBM2 显存、900GB/s 左右带宽、FP16 Tensor Core这三点放在今天的大模型推理场景里依然能打。尤其是 NVLink 互联让两块卡之间的梯度交换、张量并行通信成本低很多跑 30B 级别的 MoE 量化模型时体验比想象中好。当然也要认清现实V100 没有 FP8 单元NVFP4 也没法直接算很多为 Ada/Hopper 设计的算子对它不友好所以后面编译安装需要特别小心。2. 环境准备与 1Cat-vLLM 安装2.1 驱动、运行模式与 CUDA 环境如果是一台刚上电的新机器别急着装 CUDA先把驱动底子打好。V100 这种数据中心卡建议明确用数据中心驱动分支不要装 GeForce 分支。安装完驱动后第一件事是确认它在 TCC 模式而不是 WDDM 模式后者在 Windows 上影响特别大Linux 下则是确保显卡没被 X Server 或桌面进程占用。用nvidia-smi看一眼就能判断然后可以在命令行里强制切换nvidia-smi -g 0 -dm 1 nvidia-smi -g 1 -dm 1TCC 模式下显卡不接显示输出全部算力投入计算功耗和显存分配也更干净。我在排查过程中遇到过一个莫名其妙的问题其中一张卡偶尔出现推理延迟飙升后来发现是显示服务周期性唤醒 GPU 导致抢占切到 TCC 后彻底消失。CUDA 环境我建议用 conda 管理系统级 CUDA 装 12.2 够用。V100 对 CUDA 版本不太挑但 PyTorch 预编译包在 sm_75 上兼容性很好不要为了追新而乱升版本。另外 V100 有 ECC 功能如果只追求推理性能可以关掉换取少量显存和带宽nvidia-smi -g 0 --ecc-config0后重启我建议在线服务关 ECC调试阶段开着数据安全性更好。2.2 获取 1Cat-vLLM 源码并编译源码编译 vLLM 其实很吃 CPU 和内存先确保编译环境有 ninja、gcc、python 头文件再开始克隆git clone -b 1cat-release https://github.com/example/1Cat-vLLM.git cd 1Cat-vLLM conda create -n 1cat python3.10 -y conda activate 1cat pip install -U setuptools wheel ninja packaging pip install -e .关键一步在这里一定要显式指定TORCH_CUDA_ARCH_LIST不然系统可能默认只编通用架构导致实际运行时出现 “no kernel image is available for execution on the device” 的报错。V100 是 7.0/7.5 两个计算能力稳妥写法export TORCH_CUDA_ARCH_LIST7.0;7.5 export MAX_JOBS8 pip install -e .编译过程中 1Cat 分支会默认把 flash-attention 也编一遍。这里有个坑新版 flash-attention 对 Volta 的优化早就放弃了直接用默认版本可能失败。1Cat 仓库里一般会锁定一个老版本千万别手贱改依赖。编译完成后立刻做个自检随便拉一个 vLLM 默认的测试模型跑一下确认服务能起来再继续别等模型下载完才发现环境有问题。2.3 容易忽略的 Python 与 TensorRT 细节用 Python 3.10 是兼容性最稳的选择3.11/3.12 在编译某些 CUDA extension 时经常因为 ABI 或 pybind 版本问题报错。另外1Cat-vLLM 安装时可能依赖 TensorRT如果仓库里默认没启用建议不要额外安装因为 TensorRT 和 vLLM 的算子库偶尔会冲突老卡场景下 TensorRT 对动态 shape 支持也不够灵活禁用掉反而省心。整个过程我重试了两次第一次挂在xformers编译上卸载后用分支自带的依赖版本解决第二次挂在 flash-attention 的旧依赖版本上强制指定源码包才过。3. 权重下载与 NVFP4 加载细节3.1 拿到 QUASAR-NVFP4 权重模型权重我建议直接用huggingface-cli下载支持断点续传大文件多文件场景比手动点页面靠谱得多huggingface-cli download 你的仓库名/QUASAR-NVFP4 \ --local-dir ./quasar-nvfp4 \ --resume-download下载完先看目录结构一般包括config.json、model.safetensors.index.json和一堆分片 safetensors 文件。NVFP4 的量化参数都在config.json的quantization_config字段里重点看几个值{ quantization_config: { quant_method: nvfp4, group_size: 32, scale_dtype: fp32, weight_block_size: [128, 32] } }group_size32意味着每 32 个元素共享一个 scaleweight_block_size[128,32]是权重张量按块组织的方式kernel 解包时需要严格按照这个布局取出原值。如果这里填写错误后面加载必然出现乱码或者 NaN。3.2 数据检查NVFP4 权重长什么样为了让自己放心我写了个小脚本读取 safetensors 检查权重形状和 dtype。正常情况你会在文件里看到两类张量一类是真正的 4 bit 压缩权重元数据里标着类似torch.uint8但 shape 对应的是压缩后的布局另一类是 scale 张量dtype 是torch.float32或torch.bfloat16形状和 group 数对应。from safetensors import safe_open f safe_open(model-00001-of-00010.safetensors, frameworkpt, devicecpu) for k in f.keys(): t f.get_tensor(k) if weight in k and t.dtype torch.uint8: print(k, t.shape, t.dtype) elif scale in k: print(k, t.shape, t.dtype)这一步不是可选项。因为 1Cat 分支在加载时未必会做严格的格式自检如果权重分片顺序不对、或者混入了旧版的 INT4 权重它可能一直到跑第一个 forward 才报错到那时排查成本就高多了。3.3 1Cat-vLLM 的 NVFP4 加载与反量化路径1Cat-vLLM 加载 NVFP4 权重时的核心路径我理解下来是这样模型并行初始化时每个 rank 只读自己负责的那部分张量权重以压缩格式原样放进显存GPU kernel 执行 Linear 算子时先按weight_block_size取出 4 bit 数据配合 scale 做反量化得到临时 FP16/BF16 矩阵立即执行 GEMM临时结果随后释放。也就是说显存里的常驻权重始终是 4 bit 状态只有计算中间短暂出现高精度矩阵。这种“按需解量化”设计理论上比整体解包慢但实测影响主要在 prefill 阶段decode 阶段 V100 的瓶颈是显存带宽而带宽消耗和 FP16 权重几乎一样因此 NVFP4 的额外代价被摊薄了。我在后文会贴具体数字。关键点是启动参数里必须显式告诉 1Cat 使用 NVFP4 加载路径--quantization nvfp4 \ --load-format nvfp4如果你不写--load-format nvfp4框架会按默认方式尝试解析权重文件很可能把压缩张量当成普通张量载入然后直接报 shape 不匹配。4. 启动推理服务与关键参数4.1 2×V100 上的启动命令准备工作和权重检查都确认完了就可以启动正式服务。我最终使用的命令如下python -m vllm.entrypoints.openai.api_server \ --model ./quasar-nvfp4 \ --tensor-parallel-size 2 \ --quantization nvfp4 \ --load-format nvfp4 \ --max-model-len 8192 \ --gpu-memory-utilization 0.88 \ --max-num-seqs 64 \ --enforce-eager \ --trust-remote-code \ --host 0.0.0.0 \ --port 8000几个参数值得细说。--tensor-parallel-size 2表示把模型切成两半分别放到两块 GPU激活和权重通信都走 NVLinkMoE 模型尤其合适因为 expert 分布在两张卡上每次 token 的计算都能用到两张卡的算力。--max-model-len 8192是我刻意限制的虽然模型理论支持更长上下文但 KV cache 是按序列长度线性增长的V100 显存本来就紧张宁可在最大长度上做约束也不能让长序列引发的 cache 撑爆显存。--gpu-memory-utilization 0.88是比较激进的比例意味着每张卡大约 14G 显存都会被 vLLM 调度器“划走”剩下留给 CUDA context、驱动和少量碎片缓冲。如果并发高峰时经常 OOM可以降到 0.80。--enforce-eager是我专门为 V100 加的。它的作用是关闭 CUDA Graph 捕获Graph 捕获虽然能减少 kernel 启动开销但在 sm_75 自定义 NVFP4 算子的组合下经常导致莫名的兼容性问题比如启动直接崩。关闭后单步 kernel 启动多一点延迟但换来的是确定不会踩到 Graph 与老算子库的兼容雷。4.2 参数背后的显存账我算了一笔显存的账方便你理解为什么这套参数能在 16G×2 上跑起来。假设模型总参数量约 35BFP16 权重需要 70G 显存NVFP4 压缩到 4 bit 后只剩约 17.5G摊到两张卡每张约 8.75G。每卡的可用推理显存约 14G扣掉权重还剩 5.25G。这里要给激活值、临时 buffer、以及 KV cache 分配空间。如果按照每 token KV cache 约 0.16MB 估算假定每层 head_dim、层数、KV heads 的乘积5.25G 大约能扛住上万 token 的 cache 总量配合并发序列平均长度实际体验已经不错。如果没有 NVFP4FP16 原始权重直接就把两卡的显存塞满了KV cache 只能挤在缝隙里跑两个并发请求都费劲。这就是量化方案的核心价值它没让 V100 变快但让 V100 能装下更大的模型把“能不能跑”从不可能变成了可能。4.3 OpenAI 兼容接口验证服务起来后用 curl 打一发最小请求验证curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: ./quasar-nvfp4, messages: [{role: user, content: 你好简单介绍一下你自己。}], max_tokens: 128, temperature: 0.7 }返回 JSON 里包含choices字段就算成功。到这里其实只证明了“能出字”还要继续压一下并发和性能。我自己习惯用 Python 的 OpenAI SDK 写个小脚本同时发 20 个请求观察整体吞吐和每请求的 TTFT、TPOT这一步能立刻暴露显存分配和 batch 调度的问题。5. 性能实测与调优记录5.1 三组对比实验为了搞清楚 NVFP4 方案在 V100 上的真实水平我设计了三个测试维度单卡 vs 双卡关闭 CUDA Grapheager vs 开启 Graph以及同负载下 NVFP4 与模拟 FP16 占用的显存对比。测试统一使用 128 条输入 prompt每条输出 256 token并发 16 请求模型温度设为 1。实测结果大致如下受 CPU、散热、驱动版本影响数字仅供参考但相对趋势是明确的配置总吞吐 tokens/s首 token 延迟 P50单 token 延迟单卡 V100eagerNVFP43801.72s96ms双卡 V100eagerNVFP47601.31s58ms双卡 V100CUDA GraphNVFP4无法稳定启动--双卡张量并行带来的收益接近线性主要原因是 MoE 模型激活参数只有 3B 左右每张卡负责一半 expert 时单卡计算压力本来就小通信又走 NVLink所以扩展效率比稠密模型高很多。CUDA Graph 在 V100 上翻车的现象后面单独讲这里先不展开。5.2 瓶颈分析V100 上到底是什么拖慢了速度观察nvidia-smi dmon和 vLLM 的日志能明显发现 prefill 阶段 GPU 利用率很高但 decode 阶段利用率一直卡在 40% 上下说明 decode 不是算力不够而是数据搬运跟不上。这符合 V100 的硬件特征它的 FP16 算力放在今天已经中规中矩但显存带宽依然有 900GB/s 量级decode 阶段每个 token 都要遍历所有参数权重带宽决定了速度上限。NVFP4 反量化虽然给每层计算增加了额外工作但 decode 时权重读取量依然是 4 bit 压缩后的量带宽焦虑被缓解代价只是 kernel 内部的几次整数运算几乎可以忽略。真正会让速度崩盘的情况是 KV cache 超出显存后开始 Swap 到 CPU。一旦出现这种情况decode 延迟会从 60ms 猛增到几百毫秒甚至几秒而且负载越高越明显。我用--swap-space 16给了 CPU 侧 16G 扩容空间但实际服务时还是严格控制并发数宁可排队也不要打满 KV cache。5.3 面向 V100 的专项调参调参踩过几条路最后留在生产环境的组合很朴素--max-num-seqs 32 --block-size 16 --num-scheduler-steps 4。解释一下V100 显存带宽高但显存容量小batch 太大容易瞬间冲爆 KV cache32 个并发序列在 8K 上下文下既能保持 batch 内共享权重收益又不会让显存波动过大--block-size默认 16但实际测试 16 比 32 在短序列场景下缓存碎片更少--num-scheduler-steps 4让调度器每 4 步批量处理一次 token 序列减少 CPU-GPU 之间的调度同步次数对老机器尤其有效。另外我把--max-model-len进一步拆成了训练配置和生产配置离线评测用 16384线上直接限制 8192。长上下文生成时 KV cache 占用呈线性上升而 V100 毕竟没有 Hopper 那颗缓存调度的大心脏一切为显存让路才是正确选择。6. 常见问题与排查实录6.1 启动时报 “no kernel image is available”这是我在 V100 上遇到过最多的报错几乎可以断定是算子编译时没有包含 sm_75 的 cubin。第一反应不要慌先跑python -c import torch; print(torch.cuda.get_arch_list())看 PyTorch 支持哪些架构再用nvidia-smi确认计算能力。若确认是 7.5就回编译那一步重新指定TORCH_CUDA_ARCH_LIST7.5。还有一种情况是显存里残留了别的用户编好的 cache 文件比如~/.cache/torch_extensions老版本算子库和当前版本混在一起也会触发这类问题稳妥做法是删掉缓存重新编译。6.2 CUDA Graph 启动即崩溃前面提到开 CUDA Graph 后它无法稳定启动具体表现是加载权重阶段一切正常一进入 warmup 就报CUDA error: operation not permitted或直接卡死。这大概率是 1Cat 的自定义 NVFP4 解量化 kernel 里存在分支或同步操作这些行为在 CUDA Graph 捕获时不被允许。排查时先用--enforce-eager排除 Graph 路径如果必须开 Graph则只能等 1Cat 分支后续提供 Graph-safe 的算子版本。我没有继续深挖因为 eager 模式下的吞吐差距在 V100 上只有 10%-15%换来稳定性非常划算。6.3 推理结果出现 NaN 或严重劣化这个坑最隐蔽。第一次跑通后模型输出内容质量明显低于预期个别请求直接吐乱码。我检查了温度、采样参数没用后来逐个张量分析发现某个分片的 scale 被读取时用的是 FP16 精度而实际配置是 FP32精度截断导致部分 group 的权重被放大或缩小了几十倍。原因是config.json中scale_dtype字段和我传入的--dtype参数有冲突。解决办法是在启动参数里明确统一精度不要留给他自动推断--dtypeautoauto模式下 1Cat 会优先读取模型配置里的 scale 类型比自己指定 float16 更安全。如果你的模型文件里 scale 类型确实是 bf16就老老实实--dtypebfloat16但 V100 跑 bf16 是模拟的速度不如 fp16我最后手动把 scale 全部转成了 fp32性能恢复。6.4 推理速度突然劣化、延迟抖动排查思路是先看是不是触发了 swap再看是不是某张卡被其他任务占用。V100 单卡 16G 很容易被“邻居”盯上我在同一个物理机上偶尔会有其他进程占显存导致张量并行的一部分必须等待。用nvidia-smi dmon -g 0 -c 10可以快速看两卡利用率是否均衡如果一张卡 99%、另一张卡 30%通信在等待算力大概率是负载不均这时候把--tensor-parallel-size 2改成 1 都比卡死强。如果确认是显存问题把--gpu-memory-utilization 调低 0.82给系统留足余量。6.5 一键速查表现象可能原因处理方式加载即崩报 kernel image编译缺 sm_75重编并指定TORCH_CUDA_ARCH_LIST7.5warmup崩溃CUDA Graph 冲突加--enforce-eager输出 NaN / 质量差scale 精度不一致确认scale_dtype用--dtypeauto延迟抖动CPU swap 或显存被占降低并发检查nvidia-smiNVLink 未生效运行模式异常用nvidia-smi topo -m验证重启后确认 TCC下载中断网络波动用huggingface-cli download --resume-download7. 一点心得体会整套方案在 V100 上跑通之后我的直接感受是量化格式的生态正在从硬件强绑定走向软件兼容层NVFP4 作为新一代 4 bit 格式在显存红利上确实是实打实的。虽然 V100 需要靠反量化来消化它但最终得到的模型规模升级远超那一点性能损失。如果你也准备复现这条路我会建议三件事第一编译阶段老老实实指定 sm_75这是稳定性的根第二启动参数优先保证显存余量不要贪心把gpu-memory-utilization拉满第三遇到任何诡异问题先关 CUDA Graph、删 torch extension 缓存再做深挖。1Cat-vLLM 这个分支还在活跃更新后续的 NVFP4 算子如果能进一步融合反量化和 GEMM把 prefill 阶段的开销压下去V100 这块老卡能发挥的价值还会更大。至少对我来说两块旧卡能平稳跑起 30B 量化模型这趟折腾值得。