~/blog/hikari-ds4-oom-rollback

DeepSeek-V4-Flash on DGX Spark · part 10

[DeepSeek-V4-Flash] 上次那個白賺的引擎升級,後來 OOM 討債:我為什麼把光羽換回舊版

cat --toc

TL;DR

上一篇換 Entrpi 白賺 decode 14-16→約 20,爽。我就順手把 context 往上開——結果爆了、變超慢。查到底才發現:Entrpi 為了快把權重存第二份 78.71GiB artifacts,剛好把 119GiB 機器填到 README 寫的「~16K context」那條線;我開到 320K、512K = 20-30 倍,沒剩空間,大 prefill 一來就 OOM、兩分鐘重啟。換回單份權重的舊引擎:decode 15.5 慢一點,但留得住 11GiB、不再崩。速度是有代價的——它把留給大 context 的空間吃光了。

白話版:更快的引擎,是拿記憶體換來的

先講給沒在碰這行的朋友聽。上一篇我做了一件很爽的事:同一顆 AI 模型、同一台機器,只換掉底下那支「跑模型的程式」(引擎),速度就快了三分之一。聽起來像白賺。

但那個「快」不是變魔術變出來的。新引擎為了跑得快,開機時把模型的權重重新排一份、專門給 GPU(顯卡)用的格式——問題是,原本那份權重還躺在記憶體裡沒走。等於同一批權重,在記憶體裡存了兩份。

平常沒事,因為大部分時間用不到那麼多空間。但這台機器本來就快滿了,兩份權重一疊,連喘息的空間都沒了。而我偏偏在看它變快、玩得正爽的時候,想讓 agent 記更長的對話,把 context 往上開。一開大,那點剩餘空間瞬間就不夠——大請求一來,整台爆掉、被系統砍掉,再花兩分鐘重啟。你在那兩分鐘裡用它,當然覺得超慢。

說穿了,慢的不是模型、也不是我上次量錯速度——是那份多出來的權重把空間佔滿,我又硬要往上加 context。這篇就是我怎麼撞上這件事、怎麼查、最後為什麼換回慢一點、但留得住空間的舊引擎。


起點:看它變快看得太爽,我就想把 context 開更大

上一篇我很開心,把 ds4 引擎換成 Entrpi,decode 從 14-16 拉到約 20、prefill 大概兩倍,而且模型一個字沒動。光羽(那顆常駐在 DGX Spark 上、當我整套家用 agent 大腦的 DeepSeek-V4-Flash)一下變快,我就起了貪念:既然這麼快,不如把 context 開大一點,讓 agent 記更長的對話、翻更多歷史。

結果它就在那裡爆了、變超慢——有些回合正常,有些卡個兩分鐘。帳面上明明升級了,怎麼反而更慢?升級的東西,回頭討債了。

目標:搞懂它為什麼慢,然後在「快」跟「穩」之間選一個

這篇只有一個目標:把光羽變慢的真因挖出來,然後做一個決定——是留 Entrpi(快、但會出事),還是換回舊引擎(慢一點、但穩)。

過程中我走錯了兩條路(以為是 thinking、以為是 KV),這段繞路探底,剛好成了這篇的推演主軸。真相跟我一開始猜的完全不同。

真相不是「慢」,是龜速到像壞掉

第一個直覺是「DS4 是會思考的模型,是不是 thinking 吐太多字?要不要關掉 think?」——結果錯。thinking 從頭到尾都不是問題。

我下去量,server 自己回報的 decode 是 1.4 tok/s(正常約 20,慢了 14 倍)。但真正的指紋在這裡:GPU 使用率只有 5%、CPU 也閒著,卻只跑得出 1.4 tok/s。它不是在算,是在——模型的 working set 被換出去了,每吐一個字都要把權重從別的地方一頁一頁搬回來。難怪 /v1/models 秒回、但真的生成就卡到 timeout。

退化指紋:GPU util 5%、CPU 閒,decode 卻只有 1.4 tok/s——不是算力不夠,是 working set 被換出、每個 token 都在等權重頁回來;對照正常態 GPU 忙、decode 約 20 tok/s

兩條繞遠路:managed KV 跟一個 flag

繞路一:managed KV。 光羽當時開 512K context,Entrpi 啟動 log 自己有一行紅字:using managed KV cache for ctx=524288 ... may degrade performance。看起來就是它——512K 走了會拖慢的 managed KV 路徑。重啟一下、回到 20.4 tok/s。我把 context 從 512K 降到 320K(這個大小走的是不會退化的 native KV),想一勞永逸躲開它。

結果 320K 反而直接 OOM——systemd journal 白紙黑字 killed by the OOM killer。躲掉了會拖慢的病,換來直接崩。

繞路二:一個叫 --warm-weights 的 flag。 我抓到目前的啟動指令裡有這個 flag,直覺是它把整份權重預熱進記憶體、佔太多。讀了 Entrpi 原始碼確認機制(細節在最後的 debug 段),把它移掉——日常請求(31K prefill)確實從崩到穩了。

但故事沒完:移掉 warm-weights 只治好日常,一個更大的 prompt(211K token)還是 OOM。 我還在原地打轉。

一句話點破:「可是舊版不會 OOM 誒」

轉折是 user 丟來的一句話:「可是我舊版本的 ds4 不會 OOM 欸。」

這句直接把我從細節裡拎出來。我回頭比對舊引擎的設定——它也開 --warm-weights、也跑 256K 這種大 context,卻從來不 OOM。 所以會不會出事,不是那個 flag 決定的,是引擎本身留不留得下空間

順著這條線挖到底:Entrpi 為了把 decode 從 16 拉到 20,開機時把權重 repack 成一份 78.71GiB 的 device artifacts(給 Blackwell 的 CUDA kernel 用的排列方式)。但那份 78.71GiB 是額外多存的一份——原本 80.76GiB 的權重 mmap 還在。等於同一批權重,記憶體裡有兩份。

DGX Spark 標稱 128GB,實際可用約 119GiB。而 Entrpi 的 README 把話講得很白:這 119GiB「夠跑模型 + 16K context 的 KV」——它是照 ~16K context 抓的。 兩份權重疊起來,剛好把整台填到那條線、只剩約 2GiB;舊引擎只有一份,同一台留得住約 11GiB。

啊,原來如此。 Entrpi 那多出來的速度,是拿留給大 context 的空間換的。16K 以內都沒事——可是我偏偏看它變快、爽過了頭,把 context 開到 320K、512K,是那條線的 20-30 倍。那 2GiB 根本不夠塞我硬加上去的 context,一個大 prefill 的尖峰一來就崩;舊引擎的 11GiB 才撐得住。

兩份 vs 一份:Entrpi 為了快把權重 repack 成 78.71GiB artifacts,加上原始 80.76GiB mmap=兩份,把 119GiB 塞到只剩 2GiB→大 prefill OOM;舊引擎權重一份,留 11GiB headroom→不崩

決定:換回舊引擎,吃下那 4.5 tok/s

真相清楚了就好選。我把引擎換回舊的 ds4-186-524、跑 256K。取捨攤開來:

Entrpi v0.3.0舊 ds4-186-524(現在用)
decode~20 tok/s~15.5 tok/s
prefill761 tok/s324 tok/s
記憶體 headroom~2GiB~11GiB
大 prompt211K 就 OOM68K 穩過、還剩 11GiB
出事時OOM→systemd 兩分鐘重啟不出事

實測換回去:decode 15.5(server log 15.57/15.51)、68K 的大 prefill 全程 available 穩在 11GiB、NRestarts=0。慢了那 4.5 tok/s,但再也不會跑一跑自己 OOM、卡你兩分鐘

而且回頭想:我上次感覺「變慢」的體感,根本不是 decode 那 4.5 tok/s 的差,是那兩分鐘的 OOM 重啟。帳面上快、實際上被重啟吃掉——這種快,不要也罷。 user 裁「舊版 256K」,收工。

平心而論:這件事 Entrpi 真的沒藏,README 白紙黑字寫了那條 16K 的線;repo 也沒人回報這個 OOM。所以這不是維護者藏 bug,是我拿一顆為 16K 調的引擎去跑 20-30 倍的 context——我自己硬踩進去、README 早就寫清楚的 trade-off,不是等上游修就會好的東西。


Debug 區:給想自己查的人的碎念

前面看懂就夠用了。這段是留給想在自己機器上查同款問題的人——三個查的時候會卡住的地方。

① 查 runtime 別信報告,而且 log 可能根本不見。 現在跑的 server 的 stdout 被導到一個已經死掉的 tmux socket(/proc/<pid>/fd/1 -> socket:[...])——live log 看不到。我只能靠三樣東西拼出「現在跑的到底是哪顆引擎、卡在哪」:systemctl --user 的 journal、server 每筆 chat 自報的 timings(decode_tok_s/ttft_ms),還有 /proc/<pid>/exe 認 binary + cmdline 的 flag 指紋。中間我一度把 Entrpi 的 ds4-server 誤當成舊 binary——兩顆執行檔同名,是靠 --warm-weights/--power/--kv-disk-dir 這些 flag 指紋才分得出來。

② systemd unit 是 --user scope,不是 system。 我一開始 systemctl status ds4entrpi-8000 查,回「could not be found」,差點判定「這服務沒人看守、死了不會回來」。其實它是掛在 user scope(systemctl --user),active、enabled、Restart=on-failure 都好好的。查錯 scope 害我繞一圈。

③ warm-weights 做了什麼——為什麼我把它當實測 A/B、不當源碼證明。 這個 flag 對應 model_warm_weights(ds4.c:1915),會 MADV_WILLNEED 再逐頁 touch,開機時把整份 80GiB 權重 mmap 全部灌成常駐。但 runtime 走 device artifacts、根本不需要這份 mmap(啟動 log 那句 expert weights served from device artifacts 就是),所以把它整份灌進實體記憶體等於白吃 80GiB。我把 flag 移掉、日常 31K prefill 就從崩到穩。但我把這當實測 A/B、不是逐行源碼證明——這個 flag 確實會在開機把 80GiB fault-in 進實體記憶體,但壓力下確切的回收行為很細(clean 的 file-backed 頁其實兩邊都可回收,而且 glibc 上 posix_madvise(POSIX_MADV_DONTNEED) 是 no-op),我信量到的結果、不硬掰一條漂亮的因果鏈。它也擋不住 211K 那次 OOM,因為那份 78.71GiB artifacts 一直都在。

(順帶一提,GB10 的統一記憶體讓 RSS 這個數字很不可信:退化的時候 process RSS 只剩 791MiB,權重其實在 unified memory 裡、不算進 RSS。要判記憶體壓力,看 free 的 available 跟 journal 的 batch-fit 行,別看 RSS。)


帶走的三個重點

速度是有代價的,先問它拿什麼換。 Entrpi 的 +4 tok/s 是拿記憶體 headroom 換的。在一台本來就快滿的機器上,這筆帳我升級的時候沒算到——它把權重存了第二份。下次看到「同硬體、更快的引擎」,先問一句:它多用了多少記憶體?而且「快」通常只在一個範圍內快——Entrpi 是照 ~16K context 抓的,我卻拿它跑 20-30 倍還一臉驚訝。靠一個加速方案之前,先看清楚它是針對多大的 context 調校的。

帳面數字不等於體感。 我以為「變慢」是 decode 慢了,量了半天才發現真正拖垮體感的是那兩分鐘的 OOM 重啟。穩定,對日常 agent 流量來說,價值大於帳面 tok/s。 一顆穩穩 15.5 的腦,比一顆偶爾崩兩分鐘的 20,好用。

查機器要查 runtime 實況,別信報告。 memory 說現在跑的是 Entrpi、log 導去死掉的 socket、unit 藏在 user scope、RSS 在 UMA 上不可信——每一個都差點把我帶歪。最後是 server 自報的 timings、/proc/<pid>/exe、cmdline flag 指紋這些當下量到的事實把真相拼出來的。


這篇是 #9〈把 ds4 引擎換成 Entrpi、白賺半代升級〉 的誠實續集——上一篇我慶祝升級,這篇它來討債,而帳是我自己沒算到權重被存了兩份。

同系列其他篇 — DeepSeek-V4-Flash on DGX Spark

常見問題

Entrpi 引擎升級後為什麼會 OOM?
Entrpi 為了把 decode 從 14-16 拉到約 20 tok/s,啟動時把權重 repack 成一份 78.71GiB 的 device artifacts 給 CUDA 用——但原始的 80.76GiB mmap 還在。等於權重在記憶體裡存了兩份。DGX Spark 只有約 119GiB 可用,兩份一疊就把整台塞到只剩 2GiB。日常短請求還撐得住,一個大 prompt(我實測 211K token)的 prefill 尖峰一來就爆掉,被 OOM killer 砍掉,systemd 再花約兩分鐘自動重啟。舊的 ds4-186-524 引擎不 repack、權重只有一份,同一台就留得住約 11GiB headroom,不會 OOM。
--warm-weights 這個 flag 會害 OOM 嗎?
本身不是主因,但會加壓——它是我第一條真的線索。`--warm-weights` 對應 `model_warm_weights`(ds4.c:1915),會用 MADV_WILLNEED 加逐頁 touch,把整份 80GiB 權重 mmap 在開機時全部灌成常駐。但 runtime 其實走 device artifacts、不需要這份 mmap,把它整份 fault-in 進實體記憶體等於白吃 80GiB。移掉這個 flag,日常 31K prefill 就從崩到穩——這是**實測結果**,確切的分頁機制我不硬掰(clean 的 file-backed 頁其實兩邊都可回收,而且 glibc 上 posix_madvise(DONTNEED) 是 no-op)。它擋不住 211K 那次 OOM——因為那份 78.71GiB artifacts 一直都在,跟這個 flag 無關。
降 context 或改 KV 精度能解 OOM 嗎?
幾乎沒用。DeepSeek-V4-Flash 的 KV 本來就很小(256K 才約 5.9GiB),而且 Entrpi 預設已經是 FP8 主 KV + FP4 indexer 壓縮,沒有更省的檔位好調。真正吃掉 119GiB 的是那份 78.71GiB 的權重 artifacts,不是 KV。降 512K→256K 省的 KV 約 5.4GiB(512K≈11.3、256K≈5.9GiB)——但整台是被那 ~40GiB 的重複權重擠爆的,砍 KV 幾乎動不了大局,大 prefill 照樣 OOM。要留活路只能換回單份權重的引擎,或忍受偶爾 OOM 重啟。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。