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