Ringkasan Utama
Unified Radix Cache di SGLang mengganti satu kelas prefix cache per kombinasi attention dengan satu radix tree yang menyimpan slot FULL, SWA, dan MAMBA di setiap node, lalu melanjutkan compute dari boundary terdalam yang diterima semua komponen. HiCache menambah tier host memory dan Mooncake; SGLang melaporkan sekitar 15,5x effective input throughput pada DeepSeek-V4-Flash multi-turn.
Prefix caching tampak seperti masalah yang sudah selesai, sampai modelnya bukan lagi full attention biasa. Layer sliding window hanya butuh beberapa token terakhir sebelum titik reuse, layer recurrent butuh snapshot state tepat di titik itu, dan cache yang melayani model gaya Llama tahun lalu tidak punya cara untuk menyatakan keduanya.
Pada 2026-09-23 tim SGLang menerbitkan deep dive tentang cara Unified Radix Cache menangani hal itu, diadaptasi dari artikel LMSYS bertanggal 2026-08-11. Saya tidak menjalankan benchmark ini sendiri. Saya membaca post SGLang, dokumentasi desain HiCache, dua pull request yang dirujuknya, dan paper SGLang asli, mengambil ulang masing-masing, dan hanya memakai angka yang benar-benar terlihat di halamannya. Satu-satunya yang saya jalankan adalah toy empat puluh baris untuk aturan reuse boundary, karena aturan itu bagian yang paling layak dipahami.
Mengapa model hybrid butuh prefix cache yang berbeda
Radix cache menyimpan KV dari prompt lama dalam sebuah tree sehingga request baru bisa melewati prefill untuk prefix yang sama. Untuk full attention ini sederhana: setiap token memperhatikan semua token sebelumnya, jadi reuse membutuhkan KV sepanjang seluruh matched prefix. Model hybrid memecah kesimetrisan itu, karena tiap komponen model menyatakan syarat yang berbeda untuk reuse boundary yang sama.
| Komponen | Yang dibutuhkan reuse di boundary | Contoh pasangan model menurut SGLang |
|---|---|---|
| FULL (full attention) | KV sepanjang seluruh matched prefix | DeepSeek-V4 memasangkannya dengan SWA |
| SWA (sliding window) | Window token yang berkesinambungan tepat sebelum boundary | DeepSeek-V4 (FULL plus SWA) |
| MAMBA (recurrent state) | Snapshot recurrent state tepat di boundary | Kimi-K3 (FULL plus MAMBA); Inkling memakai ketiganya |
Desain lama menjawabnya dengan satu kelas cache khusus per kombinasi dan per kapabilitas, sehingga setiap arsitektur baru melipatgandakan kode. Desain baru adalah satu tree dengan komponen yang bisa dipasang, jadi jumlah kelas mengikuti jumlah komponen, bukan jumlah kombinasi. SGLang membagi tugasnya: UnifiedTreeCore memegang matching, split, insert, lock, dan evict, sedangkan UnifiedRadixCache mengorkestrasi memory pool.
Satu tree, satu slot per komponen, dan tombstone
Setiap node di tree menyimpan span token ditambah satu slot per komponen. Slot FULL dan SWA berisi KV page index; slot MAMBA berisi checkpoint di ujung prefix node. Setiap komponen mengurus split, insert, lock, dan evict-nya sendiri, sehingga satu komponen bisa mengembalikan memori tanpa mengganggu yang lain.
Konsekuensinya adalah tombstone. Data SWA di luar window yang dibutuhkan reuse bisa tetap di-cache atau di-evict sendiri, dan slot SWA yang di-evict dibiarkan kosong pada node yang data FULL-nya masih ada. Untuk mengambil kembali sebagian data SWA sebuah node, node itu di-split dulu di retention boundary. Checkpoint MAMBA bersifat copy-on-write: ia tetap di endpoint aslinya, dan request yang memakainya mendapat salinan privat yang kemudian diubah.
Bagi saya tombstone adalah ide paling berguna di post ini. Cache satu-kelas harus menganggap node itu ada atau tidak ada. Di sini node bisa setengah ada, dan itulah kondisi yang paling sering dialami model sliding window, karena sebagian besar prefix panjang tidak pernah dibutuhkan lagi oleh layer window.
Mencari safe reuse boundary
Matching menelusuri jalur node yang cocok, dan di setiap boundary kandidat tiap komponen memeriksa datanya sendiri secara independen. Compute dilanjutkan dari boundary terdalam yang diterima semua komponen. Contoh SGLang memakai node n3 dengan slot SWA yang di-tombstone dan node n4 tanpa checkpoint MAMBA, sehingga reuse mundur ke boundary yang lebih awal.
Toy di bawah adalah ilustrasi saya sendiri untuk aturan itu, bukan kode SGLang. Tiap node mencakup empat token, window-nya empat token (artikel memakai window empat pada diagramnya, sebagai ilustrasi dan bukan setelan benchmark), dan saya menjalankannya dengan Python 3 untuk mendapat tiga hasil cetak.
from dataclasses import dataclass
@dataclass
class Node:
tokens: int # length of the token span on this node
full: bool = True # FULL slot present (KV pages)
swa: bool = True # SWA slot present (False = tombstone)
mamba: bool = False # recurrent-state checkpoint at this node's end
def safe_boundary(path, window, use_swa=True, use_mamba=True):
"""Deepest token count from which compute can resume, or 0."""
best, pos = 0, 0
for i, node in enumerate(path):
pos += node.tokens
# FULL must be intact along the whole matched prefix.
if not all(n.full for n in path[: i + 1]):
break
ok = True
if use_swa:
# SWA needs a continuous window of tokens ending at this boundary.
need, j = window, i
while need > 0 and j >= 0:
if not path[j].swa:
ok = False
break
need -= path[j].tokens
j -= 1
# MAMBA needs a checkpoint at exactly this boundary.
if use_mamba and not node.mamba:
ok = False
if ok:
best = pos
return best
path = [
Node(4, mamba=True),
Node(4, mamba=False),
Node(4, swa=False, mamba=True), # tombstoned SWA slot
Node(4, mamba=False), # no checkpoint at this endpoint
]
print(safe_boundary(path, window=4)) # 4
print(safe_boundary(path, window=4, use_mamba=False)) # 16
print(safe_boundary(path, window=4, use_swa=False, use_mamba=False)) # 16Baca ketiga output itu bersama. Dengan SWA saja, tombstone di n3 tidak merugikan apa pun, karena window di ujung n4 hanya butuh n4. Tambahkan komponen recurrent dan hasilnya jatuh dari 16 token menjadi 4, karena satu-satunya boundary yang punya checkpoint terhalang oleh tombstone dan checkpoint yang hilang. Itulah biaya sebenarnya model hybrid: reuse dibatasi komponen yang paling ketat di tiap titik, bukan rata-ratanya.
HiCache: tiga tier di balik tree yang sama
HiCache memperluas Unified Radix Cache ke L1 (memori GPU), L2 (host memory), dan L3 (storage eksternal, Mooncake dalam benchmark SGLang). Dokumentasi desainnya tegas soal sharing, dan itu mengubah cara Anda menghitung ukuran fleet.
| Tier | Lokasinya | Dibagi antar instance? |
|---|---|---|
| L1 | Memori GPU | Tidak, privat untuk satu inference instance |
| L2 | Host memory di node milik instance itu | Tidak, instance tidak pernah membaca L2 instance lain |
| L3 | Storage backend: file, mooncake, hf3fs, nixl, aibrix, atau backend custom dynamic | Hanya jika backend dikonfigurasi untuk itu |
Request pertama-tama dicocokkan dengan L1 dan L2 di HiRadixTree. Jika panjang hit L3 melewati ambang (256 token secara default), cache melakukan prefetch dari L3 ke host memory, lalu memuat KV ke GPU sambil menumpang-tindihkan layer N+1 dengan compute layer N. Setelah prefill, hanya data yang belum ada di L3 yang ditulis ke bawah. Tiga write policy menentukan kapan: write_through menulis ke tier berikutnya segera, write_through_selective hanya setelah ambang access count terlampaui sehingga hanya data panas yang di-backup, dan write_back hanya saat eviction dari tier atas.
Untuk DeepSeek-V4, SGLang menggambarkan pool tambahan (compressed KV, indexer buffer, compressor state) sebagai sidecar yang berbagi index dan transfer dengan komponennya. Tiga sidecar mengikuti FULL dan dua mengikuti SWA, sehingga satu transfer tier memindahkan seluruh keluarganya bersamaan.
Perintah launch di bawah hanya memakai flag dan nama environment variable yang muncul di dokumentasi HiCache dan post LMSYS HiCache. Nilainya adalah placeholder yang harus Anda ganti, dan saya belum menjalankannya di GPU.
# Mooncake connection settings are read from environment variables.
# Names are from SGLang's HiCache post; every value here is yours to fill in.
export MOONCAKE_TE_META_DATA_SERVER="<metadata-server-url>"
export MOONCAKE_MASTER="<master-host:port>"
export MOONCAKE_PROTOCOL="<rdma-or-tcp>"
export MOONCAKE_DEVICE="<nic-names>"
export MOONCAKE_GLOBAL_SEGMENT_SIZE="<bytes-this-node-contributes>"
# L1 = GPU pool, L2 = host pool sized as a multiple of L1, L3 = Mooncake.
python3 -m sglang.launch_server \
--model-path "$MODEL_PATH" --tp 8 \
--page-size 64 \
--enable-hierarchical-cache \
--hicache-ratio 2 \
--hicache-write-policy write_through_selective \
--hicache-storage-backend mooncake \
--hicache-storage-prefetch-policy timeout \
--hicache-storage-backend-extra-config '{"prefetch_timeout_base": 2, "prefetch_timeout_per_ki_token": 0.1, "prefetch_timeout_max": 30}'
# --hicache-ratio must be greater than 1 (host pool relative to device pool);
# --hicache-size overrides it with an absolute GB-per-rank figure.
# write_through_selective backs up only entries past an access-count threshold.Pilih policy prefetch timeout bila Anda punya SLO latency. Dokumen menjelaskan best_effort berhenti begitu prefill bisa berjalan, wait_complete menunggu semua prefetch selesai demi hit rate, dan merekomendasikan timeout untuk SLO produksi. Batasnya bisa diatur dengan prefetch_timeout_base, prefetch_timeout_per_ki_token, dan prefetch_timeout_max, dengan default 2 detik, 0,1 detik per 1024 token, dan 30 detik.
Apa yang sebenarnya ditunjukkan angkanya
SGLang membenchmark multi-turn serving, di mana tiap client terus memperpanjang satu percakapan sehingga prefix membesar di setiap round. Metriknya effective input throughput, dan hit rate adalah cached prefix token dibagi total prompt token, dijumlahkan per round.
| Workload dan setup | Hanya L1 | L1+L2 | L1+L2+L3 (Mooncake) |
|---|---|---|---|
| DeepSeek-V4-Flash (FULL+SWA), 4xH200, TP4, 48 client, 60 round, 4.096 token input dan 16 token output per turn | 9,4K tok/s | 14,3K tok/s | 145,5K tok/s |
| Inkling-Small (FULL+SWA+MAMBA), 8xH200, TP8, 64 client, 30 round, 1.216 token input dan 64 token output per turn | 15,5K tok/s | 21,1K tok/s | 67,1K tok/s |
Angka utamanya 15,5x untuk DeepSeek-V4-Flash dengan L3 dibanding L1 saja, dengan hit rate sekitar 98 persen di round-round akhir dan rata-rata TTFT di bawah 9 detik. Untuk Inkling-Small, run L3 berakhir di hit rate 96,8 persen dan rata-rata TTFT 1,23 detik; pembagian 67,1K dengan 15,5K versi saya menghasilkan sekitar 4,3x. Di kedua run, ceritanya adalah tebing: hit rate L1 dan L1+L2 jatuh ke sekitar nol setelah round 35 sampai 40 pada workload pertama dan setelah round 18 sampai 19 pada yang kedua, karena working set percakapan aktif tidak lagi muat. Artikel menyebut kurva itu sebagai pembacaan kasar dari gambarnya.
Anggap pengali itu pernyataan tentang bentuk workload ini. Empat puluh delapan sampai enam puluh empat client dengan percakapan panjang sengaja dipilih agar memori GPU dan host meluap, dan baseline-nya adalah cache yang sudah kehilangan prefix. Angka itu memberi tahu bahwa L3 menentukan ketika working set lebih besar dari L1 plus L2, bukan bahwa traffic Anda akan melihat 15,5x.
Session-aware eviction dan Rust tree core
Dua penyempurnaan berdiri di atas tree. Session-aware eviction memungkinkan aplikasi mengirim session_id yang stabil sehingga SGLang melacak entri cache mana yang dirujuk tiap session; memanggil /close_session membuang referensi session itu, sementara shared prefix tetap dipegang session lain. Untuk eviction FULL, entri yang tidak terkunci dan memenuhi syarat diurutkan berdasarkan ada tidaknya referensi session, lalu jumlah referensi, lalu kebijakan dasar, biasanya LRU. Retensi berlaku untuk L1 dan L2; eviction L3 diatur oleh storage backend.
Dibanding HiRadixCache dengan LRU pada SWE-bench, SGLang melaporkan TTFT lebih rendah: pada DeepSeek-V4-Pro TP8, 11 persen di batch 128 (device hit naik dari 42 ke 51 persen) tetapi hanya 2,9 persen di batch 256; pada Qwen3.5-397B-A17B TP8, 13,5 persen di batch 32 (device hit dari 5 ke 34 persen) dan 16,6 persen di batch 64 (device plus host hit dari 58 ke 67 persen). Keuntungannya menyusut seiring batch membesar, itulah bentuk jujur dari sebuah optimasi eviction.
Penyempurnaan kedua memindahkan tree itu sendiri ke Rust. Rust memegang topologi radix, lock accounting per komponen, intrusive LRU list, dan eviction walk; Python tetap memegang pemetaan request ke token, alokasi KV fisik, dan orkestrasi pool. Pull request yang sudah di-merge, sgl-project/sglang#32710, di-merge pada 31 Agustus 2026 dan melaporkan 2.436 kasus shared suite lulus di tiap backend. Benchmark prototipe L1-only SGLang (200 turn, 100 token input dan 100 token output per turn, 6 trial) memberi penurunan TTFT berikut dibanding Python tree.
| Model dan tipe cache | Semua 200 turn | Turn terakhir |
|---|---|---|
| gpt-oss-20b, TP2, SWA | 38 persen lebih rendah | 42 persen (turn 176-200) |
| Qwen3-32B, TP2, full attention | 10 persen lebih rendah | 18 persen (25 terakhir) |
| Qwen3-Next-80B-A3B, TP4, hybrid SSM | 5 persen lebih rendah | 7 persen (25 terakhir) |
Urutannya patut dicatat: makin rumit pembukuan komponennya, makin banyak overhead Python yang bisa dibuang. SWA mendapat paling banyak dan full attention biasa paling sedikit.
Trade-off dan titik patahnya
L2 bukan pool bersama. Host memory privat untuk satu instance, jadi menambah replica melipatgandakan kapasitas L2 tanpa membuat replica mana pun bisa memakai cache replica lain. Hanya L3 yang bisa dibagi, dan hanya jika backend disiapkan untuk itu. Backend file secara default memakai direktori lokal node /tmp/hicache kecuali path-nya adalah shared mount, cara mudah untuk mengira Anda punya shared cache padahal tidak.
Kedua penyempurnaan itu belum bisa digabung. Session-aware caching masih berjalan di Python tree core, dan pull request Rust mencantumkan session radix cache di antara bagian yang ditunda, bersama external cache linker, C128, dan penggunaan produksi di AMD, MLX, atau NPU; kombinasi itu gagal di awal alih-alih berjalan dengan semantik yang tidak lengkap. Angka speedup Rust berasal dari pull request prototipe L1-only, yang saat saya baca berstatus draft work in progress, jadi angka 38 persen adalah klaim SGLang sendiri dan bukan angka yang bisa saya reproduksi.
Jangan membaca hit rate sebagai sinyal kualitas dengan sendirinya. Hit rate di benchmark berasal dari percakapan yang tumbuh turn demi turn. Workload prompt satu kali yang prefix-nya berbeda tidak akan menunjukkan angka seperti itu, dan cache yang lebih besar tidak menaikkan hit rate secara linear, seperti peringatan dokumentasi HiCache sendiri.
Perhatikan juga apa yang tidak diberikan post SGLang: tidak ada flag server, tidak ada perintah launch, dan tidak ada bagian keterbatasan. Semua hal operasional di atas berasal dari dokumentasi HiCache, yang sama sekali tidak menyebut model hybrid, Mamba, atau SWA. Saya tidak bisa memastikan dari sumber yang saya baca bahwa setiap opsi HiCache berperilaku sama untuk setiap komponen, jadi uji model Anda sebelum mempercayai sebuah tier.
Yang dikerjakan hari Senin
Mulai dengan mencari tahu apakah prefix reuse memang bottleneck Anda, lalu tambahkan tier satu per satu. Urutan ini menjaga setiap perubahan bisa dikaitkan dengan hasilnya.
- Ukur hit rate Anda sendiri dengan definisi SGLang, cached prefix token dibagi total prompt token, pada traffic nyata selama beberapa jam, bukan lonjakan sintetis.
- Periksa apakah model Anda hybrid. Jika memasangkan FULL dengan SWA atau MAMBA, unified tree adalah cache yang Anda inginkan; baca release notes versi Anda sebelum mengira itu sudah default.
- Aktifkan L2 dulu dengan --enable-hierarchical-cache dan --hicache-ratio, lalu bandingkan dengan L1 saja. Harapkan keuntungan sedang: langkah L2 SGLang adalah 1,5x pada satu workload dan 1,4x pada yang lain.
- Tambahkan backend L3 hanya jika working set Anda melebihi L1 plus L2, dan pastikan benar-benar dibagi antar instance. Pilih policy prefetch timeout jika Anda punya SLO TTFT.
- Jika menjalankan agent multi-turn, kirim session_id yang stabil dan panggil /close_session saat percakapan selesai, lalu pantau device hit rate pada ukuran batch nyata Anda, karena keuntungannya menyusut di batch lebih besar pada run SWE-bench SGLang.
Snippet di bawah menghitung hit rate seperti definisi SGLang, dijumlahkan per round, dari hitungan per request yang Anda catat sendiri. Ini helper buatan saya, dan inilah yang pertama saya jalankan, karena tidak butuh perubahan di server.
from collections import defaultdict
def hit_rate_by_round(requests):
"""requests: iterable of (round_no, cached_prefix_tokens, prompt_tokens).
Same definition as the SGLang post: cached prefix tokens divided by
complete prompt tokens, summed per round. Averaging per-request ratios
instead would overweight short prompts.
"""
cached, total = defaultdict(int), defaultdict(int)
for round_no, cached_tokens, prompt_tokens in requests:
cached[round_no] += cached_tokens
total[round_no] += prompt_tokens
return {r: cached[r] / total[r] for r in sorted(total)}
# Illustrative input, not measured data: a conversation that stops fitting.
sample = [(1, 0, 4096), (2, 4096, 8192), (3, 0, 12288)]
print(hit_rate_by_round(sample)) # {1: 0.0, 2: 0.5, 3: 0.0}Aturan yang saya bawa dari sini: prefix cache untuk model hybrid dibatasi komponen yang paling ketat di tiap boundary, jadi bangun cache di sekitar komponen yang bisa gagal secara independen, baru tambahkan tier. Tombstone dan checkpoint membuat reuse jujur, dan L3 membuatnya besar, tetapi angka yang dilaporkan SGLang berasal dari workload yang dirancang agar memori meluap, jadi ukur workload Anda sebelum membeli storage.
Sumber dan bacaan lanjutan
- Blog SGLang: Unified Radix Cache di SGLang, satu tree untuk prefix caching model hybrid
- Dokumentasi SGLang: desain sistem HiCache
- Blog LMSYS: SGLang HiCache, hierarchical KV caching cepat dengan storage backend pilihan Anda
- GitHub: sgl-project/sglang pull request 32710, Rust TreeCore backend dengan shared parity test
- GitHub: sgl-project/sglang pull request 29074, Rust Unified Radix Cache (L1 only), draft
- arXiv: SGLang, Efficient Execution of Structured Language Model Programs
- GitHub: kvcache-ai/Mooncake

