vatt'ghern jaskier's ballads
本文 3 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

把一台 vLLM 推論伺服器從空白機器拉到第一個 token 出來,預設大約要 2,000 秒——下載映像、解壓、import torch、把幾 GB 的權重灌進 GPU、編譯 CUDA graph。Modal 沒有把這條路上的任何一步「加速」,而是把它切成四段,每段都套一層快照,最後只剩 ~50 秒。拆掉任一層,那一層原本的 fixed overhead 就會回來。

Modal 把 GPU 冷啟動壓到 50 秒——LP、FUSE、C/R、CUDA-checkpoint

Modal 把「serverless GPU」這個曾經被認為跟物理現實衝突的 promise 拆成四個獨立的工程問題:cloud GPU 的 instance 從哪裡來、container 映像怎麼快速可讀、CPU 端的 Python process 狀態怎麼回放、GPU 端的 device memory(含模型權重、CUDA graph、KV cache)怎麼回放。四層各自解決自己那段的 fixed overhead,組合起來把 AI 推論的 replica scale-up 從 kiloseconds 拉進 tens of seconds。Q1 2026 production 數字:CPU snapshot 還原 35M 次、CPU+GPU snapshot 還原 15M 次。

naive cold start ~2,000 s ——> 四層快照後 ~50 s(40x) naive instance provisioning image pull + extract python init / import torch weights → GPU + CUDA graph 2000s + LP cut ~10²s 級 GLOP 預先解最佳化、buffer 維持 idle GPU——instance provisioning 從 hot path 移除 + FUSE lazy -60s content-addressed cache、~100 ms metadata 就能 boot、其餘 block 按需拉取 + C/R snap ~10x gVisor runsc 把 python interpreter / 已 import 的 module / global 物件序列化到 pages.img + CUDA-ckpt snap ~50 s ↓ 40x NVIDIA driver 把 device memory(weights、CUDA graph、KV cache)序列化到 host,再寫盤 點擊任一綠色方塊——四層 compose 而非 replace,拆掉一層那段 overhead 立即回來

click a layer above

LP · 責任邊界

在使用者請求到達前就把 GPU instance 配發完成——餵 scraped 雲端報價、observed task arrival rate、current supply 給 Google GLOP solver,輸出每個 (zone, instance_type) 的目標 idle count。每分鐘等級重新求解。

不負責的事:image 內容、container 行為、process 啟動。LP 只保證「instance 在 idle pool 裡可挑」。

FUSE / ImageFS · 責任邊界

把 container rootfs 以 content-addressed 形式存進多層 cache(page cache → SSD → AZ → CDN → blob)。container boot 只拉 metadata(~100 ms),block 等 read 才從近端 cache 抓。Slacker 觀察:90% 的檔案在容器生命週期裡不會被開啟。

不負責的事:process 內部狀態、CUDA context、device memory。FUSE 只回答「檔案系統的 byte」。

C/R · 責任邊界

gVisor runsc 在 user-space sentry 把 Python interpreter、已 import 的 module、global 物件序列化成 pages.img。restore 時 map 進新 process 的虛擬位址空間、回填 register、重新註冊 fd——不需要 host kernel 特殊配合。

不負責的事:GPU device memory、CUDA graph。snapshot 範圍止於 sentry 控制的「假 kernel state」。

CUDA-checkpoint · 責任邊界

NVIDIA driver 把 device memory(weights、CUDA graph、torch.compile 產出、KV cache)dump 到 host,restore 時反向 reload 回 GPU、重建 CUDA context。Modal 只跟 documented driver API 互動,不碰 NVIDIA proprietary 內部結構。

不負責的事:多 GPU + NCCL(collective 同步原語在 snapshot 點 deadlock)、超大模型的 PCIe 傳輸瓶頸。

互動圖表

四層快照把 GPU 冷啟動從 ~2,000 s 壓至 ~50 s(40×)。

Modal 稱這個框架為「GPU Allocation Utilization」——付費的 GPU-second 裡有多少真正在跑使用者程式碼。產業 baseline 在 70%,許多單租戶 deployment 落在 10–20%。問題不是 GPU 不夠強,而是供給節奏跟需求節奏不對齊。Marc Brooker 的講法是:「The cost of a system scales with its (short-term) peak traffic, but for most applications the value the system generates scales with the (long-term) average traffic.」serverless 在 CPU 世界已經把這個 gap 吃掉了——Lambda、Workers 都是把冷啟動壓到毫秒級換來的。GPU 世界的瓶頸是冷啟動時間長到無法讓 idle pool 隨需求收縮:當 cold start 是 30 分鐘,你只能 over-provision;降到 50 秒,才有「按請求收縮」的選項。

底下四個小節各自處理一層,順序對應 request 在路上撞到的順序:先有 instance(LP),才有 image(FUSE),才有 process(C/R),才有 device state(CUDA-checkpoint)。這四層只有最後兩層真的算「快照」——前兩層 LP 跟 FUSE 解的是「把工作搬離 hot path」而不是「把工作做快」。LP 在請求進來之前完成 instance provisioning,FUSE 把 image 解壓推遲到「用到才做」;C/R 跟 CUDA-checkpoint 才是真正的 snapshot/restore。下方互動小工具——四個 toggle 各自開關一層,看 end-to-end 冷啟動時間預算如何回到 naive 值;對數軸刻意把 50 s ↔ 2,000 s 拉開,讓「全開」跟「全關」的視覺差距等於 40 倍。

50 200 500 1,000 2,000 cold start budget (s, log scale) budget 50s
50seconds
四層全開——baseline 50 s,每層只剩 restore 成本,沒有 fixed overhead。
每個 toggle 對應一層;關掉它,那一層的 naive 時間(instance 配發 ~1,800 s、image pull ~60 s、python import ~80 s、device init ~30 s)回填到總預算上。對數軸刻意把 50 s ↔ 2,000 s 拉開——讓「全開」跟「全關」的視覺差距等於 40 倍而不是百分之幾。

每個 toggle 對應一層;關掉它,那一層的 naive 時間(instance 配發 ~1,800 s、image…

各層貢獻:LP +1,800 s、C/R +80 s、FUSE +60 s、CUDA +30 s;全開 ~50 s,全關 ~2,000 s。

LP:cloud buffer 的線性規劃

第一層在使用者請求送到之前就要動。AWS、GCP、Azure 上的 GPU instance 從 API 呼叫到 ready 通常要 tens of minutes——這段時間如果落在 hot path 上,後面三層做什麼都沒用。GPU 缺貨期更糟:H100、L40S 在尖峰時段直接回 capacity error,必須換 zone 重試。Modal 的解法是維持一個 idle GPU buffer、用線性規劃求解 buffer 尺寸。

Objective 與 constraint 很簡單:在「至少要提供 requested + buffer 個 GPU」的前提下最小化 total cost。輸入是三個量——cloud provider 即時報價(scrape 來)、task 到達速率、目前 supply 狀況。每個 instance 通常裝 8 顆 GPU,是 packing 單位(要 5 顆 H100 在某個 zone,最小單位仍是一整台 8x instance)。Modal 把這個 LP 餵給 Google 開源的 GLOP solver 週期性重新求解,調整每個 zone / instance type 要 pre-warm 多少 instance。GLOP 是 Google 為 OR-Tools 寫的開源 LP solver、針對「在 tight loop 反覆求解」的場景做了優化——這跟 Modal 的場景剛好對齊:價格變了要重解、task arrival rate 變了要重解、新的 GPU 型號上市要重解。

minimize  Σ (price[z][t] × instances[z][t])
subject to
  Σ gpus_per_instance × instances[z][t]
      ≥ requested[z][t] + buffer[z][t]    // 供給下界
  buffer[z][t] ≥ predicted_arrivals[z][t] × headroom
  instances[z][t] ≥ 0,  integer            // instance 不能切半

// solver:Google GLOP(GNU LP-style,simplex 為主)
// 輸入:scraped prices, observed arrivals, current supply
// 輸出:each (zone, instance_type) 的目標 idle count

把這層拿掉,使用者第一個 request 撞上的就是 cloud provider 的 instance 配發延遲——這個延遲在 GPU 缺貨期甚至會直接失敗。LP buffer 是「serverless GPU」這個 promise 能夠開始談的前提:剩下三層都假設「需要的時候,idle pool 裡有 instance 可挑」。Buffer 是按 zone × instance type 分別維持的——同樣是 H100,us-east-1aus-west-2c 的 idle pool 完全分開,因為 cross-zone 傳輸成本與延遲讓它們在實務上是不同的資源池。GPU spot price 在不同 zone、不同 instance type 之間差距可以到 2–3 倍;LP solver 可以把 demand route 到最便宜的 supply 池。對使用者來說 API 仍是 gpu="H100",後面用了哪個 zone、哪個 generation 的 H100、是 on-demand 還是 reserved,都是 platform 的決策。對 GPU 這種 instance 配發延遲遠大於 reactive loop 週期的資源,predictive 是唯一可行的路徑——這跟 Kubernetes HPA 的 reactive 模型不一樣。

FUSE:content-addressed cache 的懶載入

有 instance 之後,第二段 overhead 是 container image。典型 PyTorch + CUDA 推論框架的 image 動輒 5–10 GB——預設 Docker pull + extract 路徑在 gzip 單執行緒解壓的瓶頸下大約 100 MB/s,光是拉下來解開就要一分鐘以上。注意 gzip 解壓是 CPU-bound 而非 IO-bound:image registry 把每個 layer 包成 gzip stream,client 端必須單執行緒解到磁碟才能 union mount,多核 CPU 沒幫助、SSD 寫入頻寬也吃不滿。Modal 不走 Docker 路徑,他們用 libfuse 自己寫了一個 ImageFS——把 container rootfs 以 content-addressed 的形式存進多層 cache,container 啟動時只需要拉 metadata,檔案內容 lazy 拉。Content-addressed 的意思是 storage key 就是 content hash——同一個 base layer 在不同 image 之間共用儲存與快取,新 image 只需要儲存它真正新增的 block。

關鍵觀察來自 USENIX FAST '16 的 Slacker 論文——「The majority of the files will not be read」。一個 image 裡 90% 以上的檔案在容器整個生命週期裡不會被任何 process 開啟。Eager 拉所有 layer 等於把 90% 的頻寬花在沒人要的 byte 上。Lazy 把這個成本完全推遲——container 啟動只需要把 metadata(檔案表、permission、symlink 結構)拉下來,~100 ms 就能 ready,剩下的 block 等 application 真正 read 的時候再從 cache 抓。FUSE 的選擇代價是所有 read 都會穿過 user-space daemon、比 kernel-mode 慢;但 user-space 實作的好處是 cache 後端的拓樸可以變得很自由——page cache、本機 SSD、AZ cache server、區域 CDN、全球 blob storage。

FUSE-backed 多層 cache——越靠近 process 越快、越遠越大 tier read latency throughput scope memory page cache 0.001 – 0.1 µs 10 – 40 GiB/s per-host local SSD 100 µs 4 GiB/s per-host AZ cache server 1 ms 10 GiB/s per-AZ regional CDN 100 – 200 ms 3 – 10 GiB/s per-region blob storage 200 ms 3 – 10 GiB/s global, source of truth read_ahead_kb: 128 (default) → 32,768——讓 FUSE 一次預讀大塊資料
ImageFS 的五層 cache 表:page cache → SSD → AZ cache → regional CDN → blob storage。寬度按 hit rate 視覺化(左邊命中率最高),latency 與 throughput 都是 Modal 公開的數字。

有個小但關鍵的調整:Linux FUSE 預設 read_ahead_kb 是 128,對 container image 這種「會連續讀大塊」的 workload 太保守。Modal 調到 32 MiB,讓 kernel 一次預取的大小跟 cache 後端 round-trip 成本對齊。在 1 ms round-trip 的 AZ cache server 上預取 32 MiB 等於把 throughput 推近 10 GiB/s 上限;如果留在 128 KB,每個 block 都得吃一次 round-trip,throughput 會跌一個數量級。

# Linux FUSE 預設值(保守,為一般 desktop FS 設計)
$ cat /sys/class/bdi/0:30/read_ahead_kb
128

# Modal 為 ImageFS 調整後
$ echo 32768 > /sys/class/bdi/0:30/read_ahead_kb

# 對應的 mount 端設定(libfuse 啟動參數)
imagefs --max-readahead=$((32 * 1024 * 1024)) \
        --backend=multi-tier-cas \
        /var/lib/modal/container-rootfs

整體節省下來,FUSE 這層大約挪掉一分鐘的 image 拉取與解壓。因為 storage 是 content-addressed,常見 base image 像 pytorch/pytorch:2.5-cuda12 第一次被 pull 後,AZ cache server 與 page cache 都會留下 block——後面任何使用同 base 的 user image 的 cold start 都直接命中近端 cache。同樣地,使用者修一行 application code 觸發一次 build,產出的 image 跟前一版只差少數 block,ImageFS 把它退化成「幾個 block 的差異」。一個 production failure mode 值得提:當熱門 base image 第一次出現在某個 AZ cache server 上時會有 thundering herd——多個 container 同時 first-read 同一個 block 全部 miss 到 regional CDN。Modal 在 AZ cache server 加了 in-flight request coalescing,第二個 read 同樣 block 時 cache server 不會發起第二個遠端 fetch。

C/R:gVisor 把 CPU process snapshot 成檔案

映像準備好之後,下一段是 process 自己的啟動。對 Python 推論框架而言,這段被「import torch、import vllm、初始化 logger、建立 tokenizer」主導,從 python -c "import torch" 到 prompt 就緒通常吃掉 10–15 秒;vLLM、SGLang 這種更厚的 server 更久。Python 的 import system 在這裡尤其差勁——一個 import torch 觸發數千個 .py.so 的 stat / open / read、執行頂層 module 初始化程式碼、建立全域物件,純 single-threaded、IO-bound 與 CPU-bound 都有。Modal 的做法是 checkpoint/restore——讓使用者把「import + global init」做完之後 snapshot 整個 process,後面所有 replica 都從這份 snapshot 還原,跳過 import。

底層用 gVisor 的 runsc 而非 Linux 原生的 CRIU。CRIU(Checkpoint/Restore In Userspace)試圖 snapshot 真實 Linux kernel 的所有狀態——open fd、socket、namespace、cgroup、TCP buffer、shared memory、futex——複雜到許多 corner case 會失敗。任何使用了 io_uring、eBPF map、特定 capability 的 process 都有可能無法被 CRIU 乾淨還原。gVisor 把 Linux ABI 的一個子集用 user-space 的 sentry 重新實作,snapshot 對象變成 sentry 掌握的「假 kernel state」,量小、邊界清楚、可序列化。代價是 syscall 跑得比裸 Linux 慢(每個 syscall 都得經 sentry 翻譯),但對推論這種「import 一次、跑很多次、syscall 多半是 read/write/poll」的 workload 這個 trade 划算。使用者面的 API 是 decorator 形式:

import modal

app = modal.App()

@app.cls(enable_memory_snapshot=True)   # checkpoint global scope
class InferenceService:
    @modal.enter(snap=True)              # snapshot 在這個 method return 之後拍
    def load_model(self):
        import torch, vllm
        self.engine = vllm.LLMEngine.from_engine_args(...)

    @modal.enter(snap=False)             # 還原後才執行,不入 snapshot
    def per_replica_init(self):
        self.replica_id = os.environ["MODAL_REPLICA_ID"]

    @modal.method()
    def run(self, messages):
        return self.engine.generate(messages)

@modal.enter(snap=True) 標記的 method 跑完,gVisor 對整個 process 拍一張 snapshot——zipped archive,主檔是 pages.img,從幾十 MB 到幾 GB 不等,視 import 進來多少東西而定。後續 replica 直接從這份 image 還原,跳過所有 import 與 global init。snap=False 那段是「跟 replica 綁定的東西」(replica id、per-replica logging context、根據 env var 動的 feature flag),不該被 snapshot 因為它們在每個 replica 都不一樣。snapshot 寫出的格式很直接:pages.img 是 process 的 raw page data(heap、stack、mmap regions 的逐 byte dump),外加 manifest 描述 register 狀態、open fd、namespace 結構。snapshot 在儲存層走 ImageFS 的 content-addressed 路徑——同一 service 不同 deploy 之間大量共用 page,新 snapshot 的儲存成本 marginal 接近零,這也解釋了為什麼 700k 份 distinct GPU snapshot 在 storage 上 still tractable。實測:import torch 從 ~15 s 掉到 ~2 s(約 7x)。整 server boot 的 benchmark 對照如下:

整 server boot 平均時間——snapshot 關 / 開,數字直接取自 Modal blog(vLLM、SGLang 是 framework mean,Qwen 3 0.6B 是 CUDA-checkpoint 上的整 server median)。點欄位標題排序。
配置 快照層 snap OFF (ms) snap ON (ms) 提速
vLLMC/R95,67913,7976.9×
SGLangC/R83,71317,4864.8×
Qwen 3 0.6B (vLLM)C/R + CUDA96,00014,0006.9×
Reducto VLMC/R + CUDA70,00012,0005.8×
import torchC/R15,0002,0007.5×

互動圖表

快照後:vLLM 6.9×、SGLang 4.8×、import torch 7.5×;C/R 對 torch import 效果最大。

vLLM 從 95,679 ms 掉到 13,797 ms(~7x),SGLang 從 83,713 ms 掉到 17,486 ms(~5x)——這只是 CPU 端的 snapshot。再加 CUDA-checkpoint,1 GiB Qwen 3 0.6B 的整 server boot median 從 96 s 降到 14 s。SGLang 的 5x 比 vLLM 的 7x 略低,是因為 SGLang 的 snap-OFF baseline 本身較短(83 s vs 95 s)——絕對節省差距小,但相對倍數自然就壓低。snap-ON 仍有 ~14 s 延遲,不是 Python import,而是 ImageFS pull pages.img 加 sentry restore overhead——snapshot 不是免費的,它把延遲從「import 的 IO/CPU 成本」轉換成「page reload 的 IO 成本」。對 inference workload 這個 trade 划算,但對「啟動本來就只要 ~5 s」的小工具,snapshot 反而會比 cold start 慢。

一個一定會踩到的 caveat:snapshot 對 host CPU micro-architecture 敏感。在支援 pclmulqdq 指令的機器拍的 snapshot,restore 到不支援的 instance type 就會 SIGILL——Python interpreter 或某個 native module 在 import 時做了 CPU feature detection、把指令地址 inline 到 code path 裡。Modal 對每個 service 維持多份 snapshot 對應 CPU family,placement 時 snapshot tag 與 instance CPU family 對得上才會被排上去——這也部分解釋為什麼 distinct snapshot 數量達到 1M 級。Q1 2026:CPU-only snapshot 還原 35M 次、累計 5M+ 小時 execution、約 1M 份 distinct snapshot——平均每份 snapshot 還原 ~35 次、每次 restore 後撐 ~9 分鐘 execution,跟 inference workload「scale-up 一陣子、用完就回收」的節奏吻合。

CUDA-checkpoint:device memory(含模型權重與 CUDA graph)

CPU snapshot 把 Python interpreter、import 過的 module、PyTorch 的 tensor metadata 都還原了,但 GPU device memory(模型權重、KV cache、CUDA graph、torch.compile 產出的 PTX)仍是空的。每個 replica 還是得花 30–60 秒把 GB 級權重塞進 GPU、把 CUDA graph 重新捕獲一次。CUDA graph 捕獲尤其貴:vLLM、SGLang 都會在 boot 時針對多個 batch size、context length 預先 capture 一輪 graph,這段 CPU-GPU round-trip 動輒上百次。CUDA-checkpoint 把 device 端也納入快照——NVIDIA driver 把 device memory dump 到 host,Modal 寫進 ImageFS,restore 時反向 reload 回 device 並重建 CUDA context。restore 時的順序是:先把 CPU snapshot 還原、再把 device memory dump 從 host 拉回 GPU、最後把 CUDA context 跟 device 重新綁定。Modal 只跟 documented driver API 互動,不碰 NVIDIA proprietary 內部結構。實測:1 GiB Qwen 3 0.6B 上 median boot 從 96 s 降到 14 s——model 不大但啟動成本很高,對 audio、structured extraction、vision-language 這類「模型在 tens of GB 以下、對啟動延遲敏感」的場景,CUDA-checkpoint 的價值最被放大。

// 啟用 CUDA-checkpoint:在 cls decorator 上加 snapshot scope
@app.cls(
    enable_memory_snapshot=True,
    enable_gpu_snapshot=True,    // 連 device memory 一起拍
    gpu="L40S",
)
class VisionLanguageService:
    @modal.enter(snap=True)
    def load(self):
        import vllm
        self.engine = vllm.LLMEngine(model="Qwen/Qwen3-0.6B")
        self.engine.warmup()      // 觸發 CUDA graph capture, 一起進 snapshot

    @modal.method()
    def caption(self, image): ...

// production 注意:
//   - 拍 snapshot 之前要把 weights 從 vLLM weight loader 暫時 offload
//     回 host,driver 才能乾淨地序列化
//   - 還原後要主動 recreate KV cache(eager),不能等 lazy 觸發

限制具體:第一,只支援單 GPU——多 GPU 程式如果用了 NCCL(NVIDIA Collective Communications Library,tensor parallel / data parallel 的底層通訊),snapshot 暫停的瞬間集合通訊會 deadlock。NCCL 的 all-reduce 等 collective 是同步原語、多 GPU 必須同時參與,snapshot 凍結其中一顆其他會 hang。要解這問題,要嘛 NCCL 自己提供 quiesce 點、要嘛 NVIDIA driver 把所有參與 GPU 同步凍結,目前兩條路都還沒打通。第二,對 application 有微調要求:vLLM 在拍 snapshot 前要把 weight 從 weight loader 暫存回 host(不然 weight loader 內部的 device handle 變成 dangling reference);SGLang 也類似;KV cache 必須在 restore 後主動重建——若依賴 lazy path 在第一個 request 時才建立,cold path 的延遲又跑回來了。第三,只適合模型 tens of GB 以下的服務——超大模型把 device memory 從 host 拉回 GPU 的時間自己就是新瓶頸。Q1 2026:CPU+GPU snapshot 還原 15M 次、累計 2M+ 小時 execution、約 700k 份 distinct snapshot。

另一個工程細節:restore 時 device memory 從 host pull 回 GPU 走的是 PCIe,理論頻寬 32 GB/s(Gen4 x16)、實務上 25 GB/s。對 1 GiB 模型這段是 ~40 ms 可以忽略;對 70 GB 模型就是 ~3 s——這還沒含 CUDA context 重建。所以 GPU snapshot 的最佳區間就是「模型大小遠小於 PCIe 頻寬乘以可容忍的 cold start」這個物理約束之內。下面是 vLLM 跟 CUDA-checkpoint 的最小整合範例,凸顯 snapshot-friendly 寫法跟 naive 寫法的差別:

import vllm, modal, torch

app = modal.App()

@app.cls(
    enable_memory_snapshot=True,
    enable_gpu_snapshot=True,
    gpu="L40S",
)
class Inference:
    @modal.enter(snap=True)
    def setup(self):
        # 1. 建立 engine,weights 在這裡 load 到 GPU
        self.engine = vllm.LLMEngine.from_engine_args(
            vllm.EngineArgs(model="Qwen/Qwen3-0.6B", dtype="bfloat16")
        )

        # 2. trigger CUDA graph capture,會被 snapshot 一起拍下
        self.engine.warmup(batch_sizes=[1, 2, 4, 8], max_seq_len=2048)

        # 3. snapshot 前把 weights offload 回 host pinned memory
        #    避免 driver checkpoint 時碰到 vLLM weight loader 內部 device handle
        self.engine.offload_weights_to_host()

    @modal.enter(snap=False)
    def per_replica(self):
        # 4. restore 後第一件事:weights 從 host pull 回 GPU
        self.engine.reload_weights_to_device()

        # 5. KV cache 主動重建(不能 lazy)——避免第一個 request 撞到 cold path
        self.engine.rebuild_kv_cache(eager=True)

    @modal.method()
    def generate(self, prompt: str):
        return self.engine.generate(prompt).text

四個 hook 的順序:setup 完成所有「冷成本」(load weights、capture CUDA graph)、結尾把 weights offload 回 host;snapshot 拍照;restore 後 per_replica 把 weights pull 回 GPU、重建 KV cache。Naive 寫法(直接 setup 裡 load 完就 serve)在 snapshot path 上會踩 driver 處理 dangling device handle 的 race condition——這個 explicit offload / reload 模式是 NVIDIA driver 跟 inference framework 共同協商出來的 contract。

Reducto:把 6x 折成可營運的容量曲線

Reducto 做文件處理——把 vision-language model 用在 PDF / 表單 / 票據解析。流量極端 spiky:peak-to-average 比例非常高,平日很閒、突然某個客戶把幾十萬份文件全推上來。沒有 GPU snapshot 時冷啟動 ~70 s——這個延遲意味著要嘛 over-provision idle GPU(成本高)、要嘛使用者等 70 s 看到第一頁結果(體驗差)。啟用 CUDA-checkpoint 後降到 ~12 s(約 6x)。他們現在可以把 idle pool 拉到接近 0、由突發流量觸發 scale-up,使用者等的不是 70 s 而是 12 s——對 batch document processing 是 acceptable latency。整體 kilo-GPU 級的 workload 在 GPU Allocation Utilization 上從低於 30% 拉到 70% 以上。

Reducto:cold start 從 ~70 s ↓ ~12 s(6x)後的容量曲線變化 GPU 數 時間 → demand(實際請求) before:idle pool 必須保 70 s 緩衝, 大量閒置 GPU after:12 s scale-up, supply 緊貼 demand demand before (idle pool 高) after
Reducto 在 CUDA-checkpoint 之前必須維持大量 idle GPU 來吸收突發流量,因為 70 s 冷啟動撐不住峰谷切換;6x 之後 supply 曲線可以緊貼 demand,整體 GPU Allocation Utilization 從 ~30% 升到 70%+。

四層 stack 不是逐層「再快一點」——它把「serverless」對 GPU workload 從口號變成可營運的合約。沒有 LP buffer,6x 還不夠;沒有 FUSE,每次 scale-up 還要等 image 拉;沒有 CPU C/R,CUDA-checkpoint 還原後 Python interpreter 還要重啟。組合起來才把 cold start 拉到「跟使用者請求進來的尾延遲是同一個量級」這個臨界點。Reducto 的 70 s 拆解:framework boot ~30 s + model loading ~25 s + CUDA graph capture ~15 s。C/R 把 framework boot 從 30 s 砍到 5 s,CUDA-checkpoint 把 model loading + graph capture 從 40 s 砍到 ~6 s,加 ~1 s pages.img pull,合計 ~12 s。CUDA-checkpoint 貢獻了大頭——這也對應為什麼 vision-language model 是 CUDA-checkpoint 的甜蜜點。Cost 跟 latency 在這個例子裡是同向改善而非 trade-off:當 fixed overhead 被消除而不是 amortize 的時候,Pareto frontier 整個往內推一格。

四層 compose 的代價與邊界

每一層都有它特定的失敗 mode,這些 mode 在組合裡會互相牽動:

LP 層失準     ——> 突發流量撞到 cloud provider 配發延遲, tens of minutes
FUSE 層退化   ——> 大檔案的第一次 read 撞到 blob storage 200ms 延遲
C/R 層相容性 ——> pclmulqdq 等 CPU 指令差異, 需要 per-CPU-family snapshot
CUDA-ckpt    ——> 多 GPU + NCCL 不行, 模型過大會反成瓶頸

// 共通的 operational cost:
//   每個 service 對應一族 snapshot 而非一份
//   ImageFS 必須處理 PB 級 content-addressed 物件
//   GLOP 求解必須跟得上市場價格波動的速度

這個 stack 不會自動讓所有 inference workload 都享受 40x。多 GPU 大模型(70B+、tensor parallel)目前完全跳不上 CUDA-checkpoint 列車;NCCL 在 snapshot 點 deadlock 是個基本架構問題,需要 NVIDIA driver 或 NCCL 自己提供 quiesce 機制才能解。1M / 700k 份 distinct snapshot 暗示 ImageFS 必須當成一個 PB 級的 content-addressed object store 在維運。把 35M / 15M restore 次數換成「平均每天」,CPU restore ~390k 次/天、CPU+GPU restore ~170k 次/天——這暗示 Modal 的 scheduler 必須在亞秒級時間內為每個 restore 決定 placement(哪台 host、哪顆 GPU、要不要重 attach 既有 snapshot)。下一輪提升大概率得來自 driver 或 NCCL 本身——很多 user-space 的 cleverness 已經做到頭。把四層的「invariants」整理出來:

// 拆掉任一層 invariant 都會破:
// 
// LP invariant      :「使用者請求到達時,目標 zone 有 idle GPU」
// FUSE invariant    :「container 啟動只需要 metadata, block 可以 lazy」
// C/R invariant     :「import + global init 的成本只付一次」
// CUDA-ckpt invariant:「device memory + CUDA graph 可以 freeze/thaw」
// 
// 整 stack 的 invariant :
// 
//   end-to-end cold start ≈ Σ (per-layer restore time)
//                       而不是 Σ (per-layer naive init time)

四個 invariant 同時成立時,end-to-end cold start 從 naive 的 ~2,000 秒(per-layer naive init 之和)退化成 ~50 秒(per-layer restore 之和)。一旦某個 invariant 破了(pclmulqdq 不相容、多 GPU NCCL),那一層就 fallback 到 naive path、那段 overhead 立刻回來——這也呼應 opener 講的「拆掉任一層,那一層原本的 fixed overhead 就會回來」。Modal 各層的選擇都是「比預設更激進但更難維護」:LP 而不是固定 buffer、custom FUSE 而不是 stargz、gVisor 而不是 runc + CRIU、device checkpoint 而不是每次 reload weights。每個選擇分開看都不算特別新,但組合到 production scale、跑出 Q1 2026 的 35M + 15M 次 restore,這個工程量是真實的。不是所有 workload 都應該全開四層——照需求挑:

# 場景 A:小模型、低 QPS、要求最低 cold start
#         例:embedding service, classifier, 小型 vision
@app.cls(
    enable_memory_snapshot=True,    # 開 C/R
    enable_gpu_snapshot=True,       # 開 CUDA-checkpoint
    gpu="L40S",
)

# 場景 B:中型模型、單 GPU、高 QPS
#         例:Qwen 3, Llama 8B, vision-language
@app.cls(
    enable_memory_snapshot=True,
    enable_gpu_snapshot=True,
    gpu="H100",
    min_containers=2,               # LP buffer 永遠保 2 個 warm
)

# 場景 C:大模型、多 GPU、tensor parallel
#         例:Llama 70B+, DeepSeek
@app.cls(
    enable_memory_snapshot=True,    # CPU C/R 仍有用
    enable_gpu_snapshot=False,      # NCCL deadlock, 不開
    gpu="H100:8",
    min_containers=4,               # LP buffer 必要
)

場景 A 是 CUDA-checkpoint 的甜蜜點——小模型、啟動成本主要在 framework boot 而非 weights load。場景 B 是 Modal 主流 workload,四層全開最大化收益。場景 C 暫時只能用前三層,等 NVIDIA 解 NCCL quiesce 後才能拿到 GPU snapshot 紅利。決策矩陣是線性的:模型大小決定能不能單 GPU;單 GPU 決定能不能開 CUDA-checkpoint;CUDA-checkpoint + C/R 決定 cold start;cold start 數量級決定 LP buffer 可以多瘦;LP buffer 瘦度決定 unit economics 多漂亮。每個決定都是上一層的 input。場景 C 還有一個過渡解法:把 70B 模型用 4-bit 量化壓到能塞單 GPU(40 GB 配合 H200 的 141 GB),就可以走場景 B 的 path——犧牲 accuracy 換取部署 simplicity,等 NVIDIA 把多 GPU snapshot 通了再 fallback 回 full-precision tensor parallel。

把 Modal 跟 Lambda、Cloudflare Workers 這類 CPU-FaaS 並排可以看出另一個對比軸:CPU-FaaS 的快速啟動靠的是「V8 isolate 共享 process」(Workers)或「process pool + warm container」(Lambda),執行單位是輕量級的 isolate 或 short-lived process。GPU 走不上這條路——CUDA context、weights、graph 都太重,不可能 share 給不同租戶。所以 GPU serverless 退回到 OS-level container + snapshot 這條路徑。CPU 跟 GPU 的「快速啟動」不是同一個問題的兩個版本,而是兩條完全不同的技術路線——這對任何想做 cross-resource platform 的團隊都是重要的 design pivot。

What this enables:把 GPU 冷啟動從 ~2,000 s 壓到 ~50 s 的關鍵不是任何單一層的優化,而是承認「provisioning、image、process、device」是四個獨立的延遲源——LP 移除 instance 配發、FUSE 移除 image 解壓、C/R 移除 Python init、CUDA-checkpoint 移除 device init——讓「serverless GPU」從一個物理上不可能的口號變成可以對單租戶 inference workload 簽 SLA 的營運合約,前提是模型在單 GPU 容量內、沒有 NCCL 集合通訊。