1. Qwen3.5 系列到底改了什么架构层面的理解1.1 从 Dense 到 MoE不会只看热闹Qwen3.5 系列在技术圈里最被频繁提到的一个变化是它进一步把混合专家结构MoE做成了主力形态。很多人一听到 MoE 就觉得“参数量大效果强”但对部署工程师来说这个理解远远不够。我习惯把一个 Dense 模型理解成“全科专家”不管什么问题进来所有参数都要参与计算能力全面但成本线性增长。MoE 模型则是“医院分科会诊”总共建了几十甚至上百个专家模块输入的一个 token 进来之后由 Router路由网络先做一次判断决定把这个问题交给哪几个专家去处理只有被选中的专家才参与计算。这样带来的直接效果是模型总参数量可以做得很大但单次推理真正被激活的参数只占一小部分推理时的算力开销被有效压下来了。但这恰恰是部署时最容易产生误解的地方。很多人一看“激活参数只有 3B”就觉得 32B 总参数的模型显存需求也按 3B 来算那就大错特错了。MoE 模型在推理时所有专家模块都必须常驻显存Router 只是不把所有专家都激活而不是不加载。换句话说显存按总参数规划算力按激活参数估算。这两件事必须分开看。另外MoE 模型的性能并不仅仅取决于单次推理的参数规模还和 Router 的负载均衡策略有很大关系。如果训练时没有做好 load balance loss大量 token 会被集中路由到少数几个专家上导致某些专家过热、某些专家闲置GPU 利用率就会出现明显倾斜。我在压测时遇到过 GPU 利用率抖动剧烈的现象后来排查下来除了并发调度问题之外也有一部分原因是某些专家模块的调用频率天然偏高。社区里也有类似讨论处理办法通常是在服务端预留足够的并发缓冲避免个别 busy expert 造成整体吞吐波动。1.2 混合注意力机制与长上下文策略Qwen3.5 系列在注意力机制上也做了不少文章。传统 Transformer 每个 token 都要对整个序列做注意力计算序列一长计算量和 KV Cache 占用都按线性甚至更糟的趋势上涨。新一代模型普遍采用的做法是混合注意力一部分层使用全局注意力保留完整上下文建模能力另一部分层使用分组或稀疏注意力降低长序列下的计算复杂度。同时位置编码也在演进。为了让模型能处理几十万 token 的长上下文训练阶段通常采用渐进式扩展策略——先短上下文训练再把长度逐步拉长配合局部注意力层的设计让模型在长序列上不会出现注意力“稀释”的问题。这些架构层的变化落到部署层面就是两件事。第一KV Cache 的占比会被明显放大。过去我们用 8K 上下文KV Cache 可能只占显存的零头现在业务动不动就开 128K、256KKV Cache 的显存占用可以和模型权重平起平坐。第二prefill 阶段的计算量会随着输入长度线性上涨。也就是说用户发一个 50K 的长文档进来服务端的首 token 延迟不会因为模型用了稀疏注意力就自动降低它只是让最大值比传统结构更可控。所以长上下文能力是一个典型的“纸面参数很强工程取舍很重”的特性。你可以在模型配置里开满 256K但显卡显存不一定扛得住。后面我会专门讲 KV Cache 这部分怎么规划。1.3 为什么这些变化直接影响部署方式很多人觉得部署大模型不就是跑个官方脚本、填几个参数吗真正做过一轮就会发现MoE 和长上下文这两个特性会让每一个部署决策都变得复杂。先算一笔最简单的账。模型权重显存等于总参数量乘以每个参数占用的字节数。BF16 格式下每个参数占 2 字节一个 32B 总参数的模型权重就要占 64GB 左右。如果你只有一张 80GB 的卡看起来权重能放下但别忘了 KV Cache、激活值都是额外开销实际跑起来必然超限。所以MoE 版本几乎注定要往多卡方向走。多卡又带来新的问题张量并行Tensor Parallelism怎么切分、卡间通信走 NVLink 还是 PCIe、显存如何均衡分配。与此同时长上下文和高并发会把 KV Cache 的规划难度拉到很高的水平原本单卡就能跑通的方案在长文本场景下可能要在并发数和序列长度之间做二选一。还有一个隐蔽的坑是量化。模型官方发布的 BF16 权重体积很大生产环境往往想用 AWQ、GPTQ 之类的量化方案把体积压下来。可是量化后的模型在 MoE 结构上有个特殊脆弱点Router 本身的数值精度如果受到量化误差扰动可能会把 token 路由到错误的专家导致输出质量明显滑坡。这一点我在后面的实战部分会详细说。总而言之理解架构不是为了写 PPT而是为了后面的每一个配置参数都有据可依。2. 部署前的硬性清单算力、显存和推理引擎怎么定2.1 显存估算的方法与常见误区在拿到 Qwen3.5 系列的具体规格之后我做的第一件事不是下载权重而是先估算显存需求。公式其实非常简单往纸上列清楚就行推理总显存 ≈ 权重显存 KV Cache 显存 激活值与临时缓冲权重显存最直接参数量B× 字节数。BF16 是 2 字节FP16 也是 2 字节INT8 是 1 字节INT4 大概是 0.5 字节。一个 32B 总参数的 BF16 模型基础权重就是 64GB。KV Cache 的估算用下面的公式套KV Cache 显存 并发序列数 × 最大序列长度 × 层数 × KV 头数 × 头维度 × 2K 和 V × 每参数字节数我拿一个典型配置举例假设模型是 64 层、KV 头数 8、头维度 128BF16 格式那么每个 token 的 KV Cache 占用大约是 64 × 8 × 128 × 2 × 2 256KB。对于一个 32K 的上下文、并发 16 路的场景KV Cache 总量就是 32768 × 16 × 256KB约 128GB。这个数字是不是很吓人所以我会建议业务侧把默认 max_model_len 设置在 8K 到 16K而不是一味往上顶。这里有几个我看到过太多次的误区。第一个只看模型权重文件大小就判断“一张 80G 卡能跑”完全没把 KV Cache 算进去。第二个认为 MoE 模型激活参数少所以显存需求也少实际是所有专家都要驻留显存。第三个把 max_model_len 设成 256K 就以为“为了长文本做好准备”实际上服务会根据这个值预分配 KV Cache 空间设得越大显存被吃得越狠并发能力就越差。2.2 推理引擎选型vLLM、SGLang、TGI 的一次横向对比部署 Qwen3.5 系列这种规模的模型我不会选择直接写一个基于 HuggingFace Transformers 的推理脚本性能差距太大。市面上的高性能推理引擎各有所长我从生产落地的角度整理了一个对比。角度vLLMSGLangTGIOpenAI 兼容 API原生提供体验最完整原生提供需要额外封装显存管理PagedAttention利用率高RadixAttention 前缀复用强有自己的缓存机制MoE 支持成熟成熟成熟长上下文优化中上支持 prefix caching长前缀复用场景优势明显中规中矩社区活跃度很高踩坑资料多增长快更新频繁相对稳定上手难度低命令行即可启动中中我的选择逻辑很简单如果团队已经习惯了 OpenAI 的接口风格希望最快速度跑通一套能用的服务vLLM 是最稳妥的选项。它默认提供 /v1/chat/completions 接口很多业务代码不需要做任何改动。如果业务场景里大量出现“同一段长系统提示词 不同用户问题”的模式比如 Agent 类、文档问答类应用我会比较推荐认真试试 SGLang它的 RadixAttention 在处理重复前缀的 KV Cache 复用上效果明显。TGI 更适合已经深度绑定相关生态的团队单独为它引进一套方案反而增加维护成本。对于大多数团队来说我的建议是默认 vLLM遇到长文本重复前缀的痛点再引入 SGLang 做 AB 对比。不需要一上来就把三个引擎全铺开。2.3 软件栈版本匹配CUDA、驱动、Python 的三方对话部署服务时很多人先装一个最新的 CUDA然后跑不起来就怀疑模型有问题。实际上推理框架、PyTorch、CUDA、GPU 驱动四者之间的版本匹配才是第一个真正的坑。我这次使用的软件栈是这一套操作系统Ubuntu 22.04GPU 驱动550 系列CUDA12.4按推理框架官方预编译的版本走Python3.11推理框架vLLM 稳定版注意服务器上不会只有一个 CUDA。系统自带驱动后NVIDIA 驱动会带一套 CUDA runtime但 Python 环境里的 PyTorch 是自带 CUDA 运行库的。所以真正要关心的是 PyTorch 和推理框架轮子的版本匹配而不是系统里 nvcc 的版本。启动前用这几条命令确认环境nvidia-smi nvcc --version python -c import torch; print(torch.__version__); print(torch.version.cuda)如果输出里 torch 的 CUDA 版本和 nvidia-smi 支持的驱动版本明显不匹配很容易出 OOM 之外的各种奇怪报错。一个省事的办法是直接用官方发布的 Docker 镜像CUDA、PyTorch、推理框架都预先配好省掉大半兼容性排查。我还遇到过因为 Python 版本太低装不上推理框架最新 wheel 包的情况。所以会在部署前先建一个干净的虚拟环境比如 conda 或 venvPython 3.11 起步不要用系统自带的旧版本。3. 服务器部署实操从权重下载到第一个请求3.1 下载模型权重并设计好目录结构模型权重放在哪里、目录怎么组织看似小事实际上会影响后续多版本并行部署和模型升级。我习惯统一放在 /opt/models 下面每个模型一个目录按“模型名 版本”命名。下载权重时我优先用官方命令行工具mkdir -p /opt/models cd /opt/models # 以官方命令行工具为例下载到指定目录 huggingface-cli download 模型仓库路径 \ --local-dir /opt/models/Qwen3.5-32B-A3B \ --local-dir-use-symlinks False如果网络环境导致下载不稳定可以配置合适的镜像站点。下载完成后不要急着启动先检查关键文件是否完整ls -lh /opt/models/Qwen3.5-32B-A3B/正常情况下目录里应该有 config.json、tokenizer.json、tokenizer_config.json、模型权重分片文件safetensors 格式以及 index 文件。文件分片数量如果和官方说明对不上很可能下载不完整。我曾经遇到过一次某个 shard 文件只有正常体积的 60%启动模型时直接报“文件大小与索引不匹配”卡了好一阵子。所以我会在启动前顺手用校验工具核对文件宁可多等十分钟下载也不要带伤启动。3.2 使用 vLLM 启动 OpenAI 兼容服务环境就绪、权重就位之后就可以启动服务了。我用 vLLM 作为示例因为它最省事一个命令就能拉起 OpenAI 兼容接口。cd /opt/deploy python -m vllm.entrypoints.openai.api_server \ --model /opt/models/Qwen3.5-32B-A3B \ --served-model-name qwen3.5-32b \ --tensor-parallel-size 2 \ --dtype bfloat16 \ --max-model-len 32768 \ --gpu-memory-utilization 0.90 \ --trust-remote-code这几个参数背后都有一层判断逻辑。--tensor-parallel-size 2表示用两张 GPU 做张量并行切分因为单张 GPU 显存放不下完整权重和 KV Cache。设置这个值之前你要先确认服务器上确实有空闲的显卡并且它们之间的互联带宽不能太离谱。--dtype bfloat16是我比较推荐的选择。相比 FP16BF16 在宽范围内的数值稳定性更好预测误差在深层的传播更可控。--max-model-len 32768不能拍脑袋设。它决定了 KV Cache 的预分配空间。业务侧如果大多数请求都在 8K 以内设成 32768 就是在白白牺牲并发能力。--gpu-memory-utilization 0.90表示允许框架使用每张卡 90% 的显存剩下 10% 留给激活值和临时缓冲。太低了 KV Cache 不够用太高了容易踩到 OOM 的边缘。3.3 用 curl 和 Python 客户端验证推理链路服务启动之后不要急着接业务代码先用 curl 做最小链路验证。curl -s http://localhost:8000/v1/models返回的内容里会有一个模型列表模型名应该对应刚才传入的--served-model-name。如果这里查不到多半是模型名不匹配后续请求会一直报 404。然后发一个真实的会话请求curl -s http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen3.5-32b, messages: [{role: user, content: 用三句话解释一下 MoE 模型}], max_tokens: 256, temperature: 0.6 }返回结果里会包含choices[0].message.content字段看到正常文本就说明链路通了。业务侧接入时更常用 Python直接走 OpenAI SDK 即可from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY ) response client.chat.completions.create( modelqwen3.5-32b, messages[{role: user, content: 写一段 100 字左右的自我介绍}], max_tokens256, temperature0.7, ) print(response.choices[0].message.content)这里有一个容易被忽略的细节客户端的api_key传什么值都可以vLLM 默认不校验身份但base_url一定要以/v1结尾否则 SDK 拼接路径时会出错。4. 部署完别急着上线压测与调优的关键参数4.1 吞吐量与 TTFT两个最该盯的指标服务能返回文本只代表“能跑”不代表“能用”。上线前必须搞清楚两个核心指标首 token 延迟TTFTTime To First Token和输出吞吐量。TTFT 主要被 prefill 阶段支配。用户把一大段 prompt 发给模型服务端要先对整段输入做一次前向计算把 KV Cache 填充好然后才能开始生成第一个 token。prompt 越长prefill 的计算量越大TTFT 越高。这也是为什么很多人拿短 prompt 测延迟觉得很快一换成几千字的长文档就立刻露馅。吞吐量则是 decode 阶段的产出能力。生成阶段是逐个 token 往前推的每一步都在读 KV Cache、算注意力、采样。MoE 模型在这个阶段的优势会体现出来因为激活参数较少decode 的算力消耗低于同规模 Dense 模型。我通常会用并发脚本去压测这两个指标并用流式响应精确统计 TTFT。简单的脚本示意如下import asyncio import aiohttp import time URL http://localhost:8000/v1/chat/completions PROMPT 请写一段 300 字左右的城市交通优化建议。 async def measure_once(session): payload { model: qwen3.5-32b, messages: [{role: user, content: PROMPT}], max_tokens: 200, stream: True, } t0 time.perf_counter() ttft None async with session.post(URL, jsonpayload) as resp: async for line in resp.content: if line.startswith(bdata:) and ttft is None: ttft time.perf_counter() - t0 total time.perf_counter() - t0 return ttft, total async def main(): concurrency 8 async with aiohttp.ClientSession() as session: tasks [measure_once(session) for _ in range(concurrency)] results await asyncio.gather(*tasks) ttfts [r[0] for r in results] totals [r[1] for r in results] print(p50 ttft:, sorted(ttfts)[len(ttfts) // 2]) print(avg total:, sum(totals) / len(totals)) asyncio.run(main())注意一点只有开启了流式响应stream: true才能从 SSE 流里拿到第一个 token 的时间从而精确测出 TTFT。很多人压测用的是非流式请求得到的只是“完整响应时间”这会把 TTFT 和生成时间混在一起根本无法定位问题。4.2 显存利用率、max_model_len、tensor_parallel_size 的取舍服务启动后vLLM 会在日志里打印 KV Cache 的大小。这个数字是调参的重要依据要养成看一眼的习惯。gpu_memory_utilization一般设在 0.85 到 0.93 之间比较合理。低于 0.85KV Cache 空间可能不足并发一高就报“No available memory”高于 0.95很容易因为激活值缓冲不足直接 OOM。这个参数不是越大越好它是给“KV Cache 池子”和“剩余显存余量”之间做平衡。max_model_len的设定会直接影响 KV Cache 池的总容量。假设并发序列数是固定的把 max_model_len 从 8192 调到 32768每个序列预留的缓存空间会变成 4 倍能同时服务的序列数可能直接砍半。所以我的做法是先统计业务侧 99 分位的输入长度和最大输出长度再往上留 1.5 倍到 2 倍的余量去设置而不是直接把官方支持的上限写进去。tensor_parallel_size也不是越大越好。从 1 扩到 2 时某些权重矩阵的切分通信本来就是额外开销如果两张卡走的是 PCIe 而不是 NVLink通信延迟会更高小 batch 场景下甚至可能出现“多卡不如单卡”的情况。我建议先看nvidia-smi topo -m输出的卡间拓扑优先选 NVLink 互联的卡。最后还有一个隐藏参数--max-num-seqs它控制服务端同时处理的序列数量。调小可以降低单次 batch 的显存压力但也可能导致排队调大则能让吞吐量更接近理论峰值但 TTFT 会变高。这个值通常配合压测一起定没有固定最优值。4.3 压测脚本的常见错误别用单线程模拟真实负载我见过很多次“压测报告非常漂亮上线就崩”的情况根源在于压测方法完全脱离了真实负载。最常见的错误就是脚本里用 for 循环串行发请求。这种模式下每一个请求都要等前一个完全结束才发出本质上测的是零负载下的最低延迟而不是系统在正常并发压力下的表现。真实业务里的请求是并发到达的而且长短混合、突发性很强。第二个错误是所有压测请求用同一个 prompt。如果服务端开了 prefix caching同一个 prompt 的 KV Cache 会被复用测出来的吞吐量会虚高带有很大的误导性。正确的做法是准备一批不同长度、不同领域内容的 prompt随机抽取使用。第三个错误是只看平均延迟不看长尾延迟和错误率。在高并发下有些请求会因为 KV Cache 不足被拒绝或者因为排队时间过长超时。如果这些错误请求不算在统计里压测结果根本没有参考价值。我会在脚本里同时统计成功率、P50/P95 TTFT 和总吞吐并且建议至少跑 5 到 10 分钟只取稳定阶段的指标。刚启动时缓存还没热起来数据不够客观。5. 实战中踩过的一些坑多卡、长上下文与量化5.1 多卡张量并行的配置细节绝不是设个参数就完事第一次部署 Qwen3.5 系列时我以为把--tensor-parallel-size 2加上就完事了结果启动后服务一直 OOM而且日志里的显存占用非常不均衡。排查之后发现这台机器上一共 8 张卡其中两、三张已经被其他任务占用了而 vLLM 默认从编号最小的空闲卡开始分配最终分配到的两张卡并不在我预期的组合内。正确做法是显式指定可见的 GPU 设备export CUDA_VISIBLE_DEVICES4,5 python -m vllm.entrypoints.openai.api_server \ --model /opt/models/Qwen3.5-32B-A3B \ --served-model-name qwen3.5-32b \ --tensor-parallel-size 2 \ --dtype bfloat16 \ --max-model-len 32768 \ --gpu-memory-utilization 0.90用CUDA_VISIBLE_DEVICES把进程限制到指定的两张卡上可以避免和其他任务抢显存也能保证张量并行通信走的是预期的物理链路。启动后还要检查显存是否均匀分配nvidia-smi --query-gpuindex,memory.used,memory.total,utilization.gpu --formatcsv如果两张卡的 memory.used 差距明显要么是张量并行切分逻辑有问题要么是模型启动时某张卡上已经有残留进程。前者通常需要换卡、检查 NCCL 相关配置后者直接清理掉占用进程即可。5.2 KV Cache 与长上下文的博弈溢出、报错、降速我把 max_model_len 调到 65536 之后发生过一个很典型的报错“KV cache space is insufficient”或者 “Cannot find a free slot for new request”。原因就在于 vLLM 默认按最大序列长度为每个请求预留 KV Cache65536 的长度直接吃空了显存池并发稍微上来一点就再也塞不进新请求了。这个问题的根子是显存规划里没有区分“模型支持的上限”和“业务需要的实际长度”。模型支持 256K 上下文不代表你要让所有请求都能跑到 256K。更合理的做法是先压一个接近业务上限的序列长度比如 16K 或 32K确认稳定性后再逐步放开。如果业务里确实存在大量长文档场景我建议开起 prefix caching 来缓解--enable-prefix-caching这个功能可以让多个请求在共享前缀时复用 KV Cache而不是每次重复 prefill。对于“固定的系统提示词 用户问题”这类结构效果非常明显。还有一个容易被忽略的优化是 chunked prefill。默认情况下一个超长请求的 prefill 会独占 GPU 计算资源其余请求只能在后台干等TTFT 被拉得很高。开启 chunked 之后框架会把超长 prefill 切分成块与 decode 交错执行虽然单个长请求的生成时机被延后了一点但整体服务的尾延迟会平滑很多。5.3 量化方案搬到生产环境之后的精度观察为了让更大尺寸的模型能塞进有限显存很多团队会把 BF16 权重压成 AWQ 或 GPTQ 格式。Qwen3.5 系列部署到一半时我也试过 4bit 量化模型体积确实降了不少但业务线的同事反馈某几类提问的回复质量明显下降甚至出现数字问题胡编的情况。这里的关键不是“量化不行”而是“量化后必须做回归验证”。我遇到的问题是量化误差在网络前几层的 Router 中会被放大从而改变了一些 token 的专家路由决策。最直接的表现是量化后模型在常规问答上和 BF16 版本差别不大但只要问题涉及精确计算或强逻辑链错误率就上去了。所以我的建议是准备一份贴近线上业务形态的验证集内容包括格式要求、数学题、长文档摘要、多轮改写等。然后对同样的输入分别用 BF16 服务和量化服务跑一遍对比输出质量。如果差异可控量化方案才值得上线。如果精度损失明显可以退一步用 8bit 量化代替 4bit或者在服务端保留一个 BF16 版本用于高价值请求量化版本用于短文本、低敏感场景。另一个思路是把量化后的模型作为单独的 served-model-name 部署比如qwen3.5-32b-awq业务侧按需切换这样就能用真实流量持续观察而不需要一次性拍板。提示量化参数在不同推理框架版本里命名有差异启动前一定要查对应版本的文档避免把量化权重当成全精度权重加载结果白白占满显存。把 Qwen3.5 系列从下载到部署、压测、调优完整跑过一轮之后我最大的体会是模型架构的进化虽然值得关注但决定线上体验好不好的往往是你对显存规划、并发模型和量化精度之间取舍的敏感度。建议第一次上手的人先用小尺寸模型在单卡或双卡上把完整链路跑通再逐步往大模型和长上下文场景推进。压测从第一天开始就按并发做别等服务上线了再补课。后面如果有机会我还会整理一份关于 Qwen3.5 系列在 Agent 和工具调用场景下的服务化调度笔记到时再和大家细聊。