戰(zhàn):一條 vLLM 命令吃下 75 萬 token 的 NUMA 拓?fù)鋵?shí)測(cè))
1. 單機(jī)多卡跑 Qwen3 長上下文為什么總是卡在顯存和帶寬上如果你最近在折騰 Qwen3 這類 27B 級(jí)別的模型想在單機(jī) 4 卡上把上下文拉到幾十萬 token大概率會(huì)遇到兩個(gè)現(xiàn)象一是啟動(dòng)時(shí)顯存看著夠跑起來 KV Cache 一漲就 OOM二是單卡 benchmark 數(shù)字很漂亮一上 TP4 吞吐連 3 倍都不到GPU-Util 還長期趴在 40% 以下。這兩個(gè)現(xiàn)象背后其實(shí)是同一件事——長上下文推理的瓶頸從來不是算力而是顯存容量和多卡通信帶寬。我這次實(shí)測(cè)的環(huán)境是 4 張 RTX 4090 加單路 EPYC模型用的是帶 W4A16 量化、vision 和 mtp 頭的 Qwen3 27B 級(jí)權(quán)重。目標(biāo)很明確讓這套配置穩(wěn)定吃下 75 萬 token 的輸入同時(shí)把首 token 延遲TTFT和吞吐控制在一個(gè)能接受的范圍。整個(gè)過程里真正決定成敗的不是某個(gè)神奇參數(shù)而是三件事——KV Cache 容量賬算得準(zhǔn)不準(zhǔn)、調(diào)度器保護(hù)開關(guān)關(guān)得對(duì)不對(duì)、NUMA 拓?fù)溆袥]有被忽略。先說結(jié)論75 萬 token 能不能跑取決于你的 GPU KV Cache 裝不裝得下一條滿載請(qǐng)求。以 64 層、8 個(gè) KV 頭、head_dim 128 的典型配置估算FP8 KV Cache 下單個(gè) token 的 KV 占用大約是 131072 字節(jié)64 層 × 2 × 8 頭 × 128 維 × 1 字節(jié)。4 卡各分 10 GiB 給 KV總共約 42.9 GB折算下來大概 32.8 萬 token。也就是說一條 75 萬 token 的請(qǐng)求光靠 GPU 層是裝不下的必須靠 CPU 內(nèi)存和本地盤做分層卸載。這就是為什么后面那條啟動(dòng)命令里KV 卸載配置和調(diào)度器開關(guān)必須成對(duì)出現(xiàn)。這一節(jié)先把問題定義清楚你要解決的不是怎么把 max-model-len 寫成 750000而是在給定顯存預(yù)算下怎么讓長請(qǐng)求不 OOM、短請(qǐng)求不掉質(zhì)量、多卡通信不拖后腿。想明白這個(gè)后面的參數(shù)才有意義。2. TaoToken 前置準(zhǔn)備模型權(quán)重、API Key 與接入文檔在動(dòng)手改 vLLM 啟動(dòng)參數(shù)之前有一件事值得先做——把模型調(diào)用鏈路的憑證和文檔準(zhǔn)備好。我這次實(shí)測(cè)用的是 TaoToken 的 API 通道來對(duì)照驗(yàn)證模型輸出這樣可以在本地 vLLM 實(shí)例和線上通道之間做交叉比對(duì)確認(rèn)長上下文下模型行為是否一致。具體操作上你需要先拿到 API Key。打開 https://taotoken.net/api-keys 登錄后創(chuàng)建一個(gè)新的 Key復(fù)制出來保存好。這個(gè) Key 后面會(huì)用在 OpenAI 兼容的客戶端里格式是sk-開頭的一串字符。注意不要在代碼里硬編碼用環(huán)境變量傳進(jìn)去。拿到 Key 之后Base URL 填https://taotoken.net/api這是 OpenAI 兼容接口的根地址。Model ID 根據(jù)你要驗(yàn)證的模型填比如qwen3-27b這類標(biāo)識(shí)。這三件套——Base URL、API Key、Model ID——是任何 OpenAI 兼容客戶端接入的必備信息缺一不可。如果你對(duì)參數(shù)含義或者接入方式有疑問可以查接入文檔https://taotoken.net/doc 。文檔里對(duì)請(qǐng)求格式、流式輸出、錯(cuò)誤碼都有說明遇到 401 或者 model not found 的時(shí)候先翻這里。為什么要先做這一步因?yàn)殚L上下文部署最容易出的問題不是啟動(dòng)失敗而是啟動(dòng)成功了但輸出不對(duì)。本地 vLLM 實(shí)例和線上通道同時(shí)跑同一段長 prompt對(duì)比輸出質(zhì)量能幫你快速判斷是模型本身的問題還是部署配置的問題。比如你開了 YaRN 之后發(fā)現(xiàn)短文本質(zhì)量下降用線上通道跑同樣的短文本如果線上正常、本地掉點(diǎn)那基本可以確定是靜態(tài) YaRN 的縮放副作用。另外如果你打算長期跑編碼或者 Agent 場景可以了解一下 Coding Planhttps://taotoken.net/coding-plan 。它適合那種需要持續(xù)調(diào)用、對(duì)穩(wěn)定性和額度有要求的用法跟本地部署是互補(bǔ)關(guān)系——本地負(fù)責(zé)低延遲批量推理線上通道負(fù)責(zé)兜底和對(duì)照。3. 可復(fù)制的 vLLM 啟動(dòng)配置從 KV 卸載到 NUMA 綁核這一節(jié)給出完整的啟動(dòng)命令和配置文件。先看命令我把它拆成帶續(xù)行符的形式方便閱讀粘貼回 shell 時(shí)注意 JSON 參數(shù)外層必須用單引號(hào)包住否則 shell 會(huì)吃掉雙引號(hào)。vllm serve /data/vllm_models/Qwen3-VL-27B-W4A16-mtp \ --served-model-name qwen3-27b \ --tensor-parallel-size 4 \ --max-model-len 750000 \ --gpu-memory-utilization 0.85 \ --kv-cache-dtype fp8_e4m3 \ --kv-cache-memory 10737418240 \ --enable-prefix-caching \ --enable-chunked-prefill \ --max-num-batched-tokens 8192 \ --max-num-seqs 96 \ --no-scheduler-reserve-full-isl \ --kv-transfer-config { kv_connector: OffloadingConnector, kv_role: kv_both, kv_connector_extra_config: { cpu_bytes_to_use: 214748364800, eviction_policy: arc, offload_prompt_only: true, n_read_threads: 16, n_write_threads: 4 } } \ --speculative-config { method: mtp, num_speculative_tokens: 3, use_local_argmax_reduction: true } \ --rope-scaling { rope_type: yarn, factor: 3.0, original_max_position_embeddings: 262144 } \ --limit-mm-per-prompt {image: 40} \ --mm-processor-kwargs {truncation: false} \ --mm-ipc-gpu-memory-gb 0 \ --compilation-config {cudagraph_mode: PIECEWISE} \ --disable-custom-all-reduce \ --reasoning-parser qwen3 \ --tool-call-parser qwen3_coder \ --enable-auto-tool-choice \ --enable-prompt-tokens-details這段命令里--kv-cache-memory 10737418240是每卡 10 GiB4 卡合計(jì)約 42.9 GB。cpu_bytes_to_use是 200 GiB注意這是所有 worker 加起來的總量不是每卡 200 GB。eviction_policy用 arc對(duì)周期性訪問比默認(rèn) lru 更友好。offload_prompt_only默認(rèn) true只卸載 prefill 塊decode 塊不卸載。關(guān)于 RoPE 縮放factor 3.0和max-model-len 750000是對(duì)得上的。YaRN 的外推邏輯是max-model-len original_max_position_embeddings × factor262144 × 3.0 786432750000 留了余量。這里有個(gè)坑Qwen3-VL 用的是 Interleaved-MRoPEposition id 增長比普通 RoPE 慢所以 factor 要取得比直覺值小。同樣從 256K 外推到 1M普通 RoPE 用 4.0這里只能用 2.0 到 3.0。如果你用的是 Qwen3-Next 系列權(quán)重method要改成qwen3_next_mtp。啟動(dòng)失敗時(shí)先試另一個(gè)名字。另外 KV Cache 大小這個(gè)參數(shù)在不同版本里名字不一樣看到--kv-cache-memory用前者看到--kv-cache-memory-bytes用后者語義完全相同。NUMA 綁核這塊如果你的機(jī)器是雙路 CPU建議用numactl把進(jìn)程綁到跟 GPU 同一側(cè)的 NUMA 節(jié)點(diǎn)。先跑nvidia-smi topo -m看拓?fù)淙绻仃嚴(yán)锍霈F(xiàn) SYS說明卡跨了 NUMA 邊界。這時(shí)候可以考慮拆成 2 × TP2每個(gè)實(shí)例綁本地 NUMAall-reduce 路徑從每輪 2 跳跨 NUMA 降到 0 跳。4. 驗(yàn)證請(qǐng)求與成功結(jié)果75 萬 token 實(shí)測(cè)數(shù)據(jù)配置寫好了接下來要驗(yàn)證它真的能跑。先啟動(dòng)服務(wù)觀察日志里必須確認(rèn)的幾行。第一行是 FP8 prefill attention 是否生效日志里會(huì)打印類似Using FP8 prefill attention的確認(rèn)信息。沒看到這行說明--kv-cache-dtype和 attention backend 沒對(duì)上。第二行是 KV Cache 容量會(huì)打印GPU KV cache size: 327,680 tokens這樣的數(shù)字。第三行是 speculative acceptance rateMTP 生效后會(huì)周期性打印接受率。啟動(dòng)完成后用一段長文本做驗(yàn)證。我構(gòu)造了一個(gè)約 75 萬 token 的輸入用 curl 發(fā)請(qǐng)求curl -X POST http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen3-27b, messages: [{role: user, content: $(cat long_prompt.txt)}], max_tokens: 256, temperature: 0.7, top_p: 0.8, top_k: 20 }實(shí)測(cè)下來75 萬 token 輸入的 TTFT 大約在 40 到 60 秒之間取決于前綴緩存命中率。如果前綴完全沒命中chunked prefill 要切 92 刀750000 ÷ 8192 ≈ 92每刀一次 forward累積起來就是這個(gè)量級(jí)。如果前綴命中率高TTFT 能壓到 10 秒以內(nèi)。輸出吞吐方面decode 階段在 MTP 接受率 70% 左右時(shí)單實(shí)例能跑到 25 到 35 token/s。這里要提醒一點(diǎn)--max-num-seqs 96在這套配置下幾乎永遠(yuǎn)到不了。真正卡住你的是 KV Cache 容量。以平均輸入長度 32K 算GPU 層只能容納 10 條并發(fā)加上 CPU 層能到 60 條。所以評(píng)估這套配置不要問能撐多少并發(fā)要問我的平均 ISL 乘上需要的并發(fā)數(shù)有沒有超過 32.8 萬 token。驗(yàn)證模型輸出質(zhì)量的時(shí)候我建議同時(shí)用線上通道跑一遍同樣的 prompt。打開 https://taotoken.net/models 可以看模型列表和對(duì)話入口把長文本貼進(jìn)去對(duì)比輸出。如果本地輸出明顯變差優(yōu)先檢查 YaRN factor 和 RoPE 配置。5. 本篇常見錯(cuò)誤排查401、local proxy failed、reading choices、OAuth長上下文部署的報(bào)錯(cuò)往往不在模型本身而在接入鏈路和配置細(xì)節(jié)。這一節(jié)列幾個(gè)我踩過的坑。第一個(gè)是 401 Unauthorized。如果你在客戶端里調(diào) TaoToken 的 API 報(bào) 401先檢查 API Key 有沒有帶對(duì)。Base URL 是https://taotoken.net/api注意結(jié)尾不要多加/v1有些客戶端會(huì)自動(dòng)補(bǔ)。Key 放在Authorization: Bearer sk-xxx頭里。如果 Key 沒問題還是 401去 https://taotoken.net/console 看下額度是不是用完了。第二個(gè)是 local proxy failed。這個(gè)報(bào)錯(cuò)通常出現(xiàn)在客戶端配置了本地代理但代理沒起來的時(shí)候。檢查你的環(huán)境變量HTTP_PROXY和HTTPS_PROXY如果不需要代理就清掉。注意這里說的是本地網(wǎng)絡(luò)配置跟模型部署無關(guān)。第三個(gè)是 reading choices 相關(guān)報(bào)錯(cuò)典型信息是Error reading choices from response或者choices is null。這通常是服務(wù)端返回了非標(biāo)準(zhǔn)格式或者流式輸出被中途截?cái)?。先確認(rèn) vLLM 實(shí)例的/v1/models能正常返回再用非流式請(qǐng)求試一次。如果非流式正常、流式報(bào)錯(cuò)檢查客戶端的 SSE 解析邏輯。第四個(gè)是 OAuth 相關(guān)錯(cuò)誤。如果你用的是 Claude Code 或者某些需要 OAuth 的客戶端報(bào)OAuth token expired或者invalid_grant需要重新走一遍授權(quán)流程。這類客戶端接入時(shí)Base URL、API Key、Model ID 三件套要填全。比如 Claude Code 的配置里ANTHROPIC_BASE_URL填https://taotoken.net/apiANTHROPIC_API_KEY填你的 KeyModel ID 填對(duì)應(yīng)模型標(biāo)識(shí)。還有一個(gè)容易忽略的報(bào)錯(cuò)是啟動(dòng)時(shí)的CUDA error: an illegal memory access was encountered。這個(gè)在長上下文加多模態(tài)加投機(jī)解碼的組合下容易出現(xiàn)加上--compilation-config {cudagraph_mode: PIECEWISE}可以解決。PIECEWISE 的含義是 attention 保持 eager其余算子進(jìn) cudagraph兼容性最好。最后提醒一個(gè)參數(shù)名版本問題。--kv-cache-memory和--kv-cache-memory-bytes在不同 vLLM 版本里名字不一樣報(bào)unrecognized arguments的時(shí)候先確認(rèn)版本。另外一旦顯式指定了這個(gè)值--gpu-memory-utilization就不再參與 KV 容量推算但仍然作為進(jìn)程顯存總上限生效。6. 長上下文部署的取舍思路與后續(xù)接入這套配置跑下來我最大的感受是長上下文從來不是把參數(shù)拉滿而是一道關(guān)于取舍的應(yīng)用題。75 萬 token 這個(gè)數(shù)字本身沒有意義有意義的是你為了支撐它付出了什么代價(jià)以及這些代價(jià)在你的業(yè)務(wù)場景里是否劃算。第一個(gè)取舍是容量和質(zhì)量的平衡。開了 YaRN factor 3.0 之后你那些 2K、4K 的短請(qǐng)求質(zhì)量也會(huì)被縮放白白損失精度。如果你的業(yè)務(wù)絕大多數(shù)請(qǐng)求在 32K 以內(nèi)建議把 factor 降到 1.5 甚至不開 YaRNmax-model-len 跟著降到真實(shí)水位。如果長短請(qǐng)求混跑拆成兩套實(shí)例網(wǎng)關(guān)按 prompt 長度路由。第二個(gè)取舍是分層卸載的收益驗(yàn)證。磁盤層不是便宜顯存它的收益取決于 CPU 帶寬、NUMA 拓?fù)?、塊大小、IO 并發(fā)、命中率分布。建議先只開 CPU 主層壓一輪記錄命中率和 P99再打開 FS 層對(duì)比同一壓測(cè)下的吞吐和 P99只有正收益才保留。第三個(gè)取舍是 NUMA 拓?fù)?。三層的容量賬算得再準(zhǔn)卡插在 NUMA 邊界兩側(cè)收益照樣腰斬。上線前務(wù)必跑一次nvidia-smi topo -m看到 SYS 就說明多卡收益正在被 NUMA 邊界吃掉。這時(shí)候可以考慮去掉--disable-custom-all-reduce或者換 NCCL 算法或者拆成 2 × TP2。后續(xù)接入方面如果你需要長期跑編碼或者 Agent 場景可以看看 Coding Planhttps://taotoken.net/coding-plan 。本地部署負(fù)責(zé)低延遲批量推理線上通道負(fù)責(zé)兜底和對(duì)照兩者互補(bǔ)。模型對(duì)話入口在 https://taotoken.net/models 接入文檔在 https://taotoken.net/doc API Key 管理在 https://taotoken.net/api-keys 。遇到接入問題先翻文檔大部分報(bào)錯(cuò)都有對(duì)應(yīng)說明。最后留一個(gè)自檢清單啟動(dòng)日志里確認(rèn) FP8 prefill attention 生效、GPU KV cache size 數(shù)字、speculative acceptance rate上線前跑一次nvidia-smi topo -m壓測(cè)重點(diǎn)看 P99 TTFT、前綴緩存命中率、MTP 接受率而不是平均吞吐。這三步做完你的長上下文服務(wù)基本就穩(wěn)了。