Skip to content

[Unified Tree] Port SWA Branching-Point Caching to the Rust TreeCore - #37584

Merged
ispobock merged 18 commits into
sgl-project:mainfrom
alphabetc1:feat/rust-swa-branching
Sep 12, 2026
Merged

ispobock merged 18 commits into
sgl-project:mainfrom
alphabetc1:feat/rust-swa-branching

Conversation

@alphabetc1

@alphabetc1 alphabetc1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Motivation

#34565 added SWA branching-point caching entirely in Python. The Rust TreeCore
(SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND=rust) is a parallel implementation of the
same interface, not a binding over the Python one, so it did not inherit any of it:

  • rust/sglang-radix-tree/src/components/swa.rs did a single-node BackupHost
    with no dirty window and no nodes_to_load
  • SwaComponent had no needs_incremental_backup override, so it used the trait
    default false
  • MatchResult.swa_branching_seqlen, InsertParams.swa_branching_seqlen and
    InsertResult.swa_branch_inserted did not exist anywhere in the crate

The adapter builds its result objects from an explicit field list, so the missing
fields did not raise; they defaulted to None/False. On the Rust backend #34565 was
therefore a silent no-op: no crash, no log, no branching-point caching. The e2e
below shows exactly that on main (arm main_rust) and that this PR closes the gap.

Changes

Contract fields, threaded core -> bindings -> adapter the way mamba_branching_seqlen
already is:

  • MatchResult.swa_branching_seqlen
  • InsertParams.swa_branching_seqlen (carried across the resumable insert's walk state)
  • InsertResult.swa_branch_inserted

components/swa.rs:

  • collect_unbacked_swa_nodes_in_window_: the device-only nodes within one sliding
    window, stopping at a node an in-flight backup already owns, so two acks never claim
    the same node
  • needs_incremental_backup overrides the trait default with !unbacked.is_empty()
  • finalize_match_result_in_tree_core computes the page-aligned branching point
  • commit_insert_component_data reports whether the insert reached that boundary
  • BackupHost build emits the whole window ancestors-first with nodes_to_load; commit
    scatters the acked host span back across those nodes by device length

Write-back: #34565 dropped not self.is_write_back from _should_backup_after_insert
and restricted the write-back incremental check to SWA (Full and Mamba defer to eviction,
SWA cannot because out-of-window frees never reach eviction). The Rust core still carried
the old !self.is_write_back gate, so under write-back a reconstructed SWA window was
never published and its device slots could be freed with the host tier still tombstoned.
should_backup_after_insert_ / needs_incremental_component_backup_ now mirror the
Python core; two native tests pin both halves (SWA backs up, Mamba still defers).

Buffer mode: #34565's SWA component reads
self.cache.host_memory_mode, which the Rust component cannot; it has no cache handle.
The flag moves onto the tree core (set_host_memory_buffer_only() plus a read-only
is_host_memory_buffer_only on both backends) and both components, plus the Python
prefetch path, now read the same source. Without this, buffer_mode/pipeline.py sizes
its storage keys off a windowed transfer and writes the wrong number of pages.

Tests. #34565 gated three shared-suite tests behind _skip_swa_branching_on_rust.
The gate is gone; the tests now drive eviction and the write-through pending id through
the backend-neutral test inspector (evict_component, get_write_through_pending_id)
instead of resolve_node_handle() / node_by_id, so the same assertions run on both
cores. One test in test_rust_tree_core_integration.py pins the swa_branch_inserted /
swa_branching_seqlen field threading through the PyO3 binding; the window and scatter
behavior is covered by the native tests and the shared suite.

Validation

All on one H200 devbox (lmsysorg/sglang:dev), this branch rebased on main at 756d0e0.

main this PR
Rust native cargo test 847 passed, 0 failed
test_rust_tree_core_integration.py 102 passed
test_unified_radix_cache_unittest.py, Python core 1191 passed, 1345 skipped
test_rust_unified_radix_cache_unittest.py, Rust core 2536 ran, OK (skipped=1350) 2536 ran, OK (skipped=1346)

The skip delta of four is exactly the three un-gated tests on their matching fixtures
(Test_FULL_SWA_ps1_sw4 x3, Test_FULL_SWA_ps4_sw4 x1), identical to what the Python
entry executes. pre-commit (ruff, clippy -D warnings, rustfmt) passes.

E2E: the branching fingerprint on DSV4-Flash

DeepSeek-V4-Flash-0731, 4xH200, DSPARK, --chunked-prefill-size 16384,
--swa-full-tokens-ratio 0.1, SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS=True;
same box, same command, four arms = {main 756d0e0, this PR} x
{SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND=python, rust}. Each arm's log confirms the
tree core it ran (Tree Core: UnifiedTreeCore / RustUnifiedTreeCore).

Probe (/generate with input_ids, max_new_tokens=1, read meta_info.cached_tokens):
A seeds a 24576-token shared prefix followed by a 16384-token question (40960 tokens = 3
prefill chunks, so the prefix end sits in an interior chunk and its SWA gets freed);
B, C, D reuse the prefix with fresh questions. Two groups per arm, results identical
across groups.

arm tree core B cached C cached D cached C/D latency
main, python UnifiedTreeCore 16384 24576 24576 0.905 s
main, rust RustUnifiedTreeCore 16384 16384 16384 1.353 s
this PR, rust RustUnifiedTreeCore 16384 24576 24576 0.905 s
this PR, python UnifiedTreeCore 16384 24576 24576 0.905 s

B = 16384 then C/D = 24576 is the branching fingerprint: B hits Full for the whole prefix,
gets a branching point, and its insert re-publishes the SWA window; C and D then hit the
full 24576. On main the Rust core never gets past 16384 (the silent no-op from the
Motivation section). With this PR the Rust core matches the Python core byte for byte,
and the Python core has the same behavior for this Full+SWA configuration.

Shared-prefix bench (generated-shared-prefix, 8 groups x 8 prompts,
system 24576 / question 16384 / output 128, request rate 2, concurrency 8; two passes
per arm, warm pass reported, cold pass is JIT-confounded):

arm cache hit rate req/s mean E2E
main, rust 36.6% 0.65 11995 ms
this PR, rust 46.5% 0.76 10176 ms
this PR, python 47.1% 0.76 10247 ms

(main, python ran first on the box and its warm pass still carried first-arm JIT cost,
46.7% / 0.64 req/s; its hit rate matches the PR arms, which is the number that matters.)
Zero CUDA out of memory / illegal memory / scheduler exceptions in any arm.

Hybrid SWA/Mamba checkpoint correctness

Fixes #38815. In hybrid caches, SWAComponent.prepare_for_caching_req leaves the insertion length to Mamba after preserving the SWA eviction boundary. This keeps each checkpoint attached to its actual prefix depth. Full+SWA caches without Mamba retain branching-point caching.

An ad hoc E2E experiment ran real Inkling requests with Triton, page size 1, and the Rust TreeCore. Checkpoints at 64/128, a branch at 96, and prefill to 192 previously attached state192 to key96. A subsequent probe reused 96 tokens and could change greedy output. The guarded insertion stores key192/state192 and the probe correctly resumes at checkpoint64. Across three input corpora, warm output token IDs and logprobs now match cold-cache results exactly.

Validation for the guard: the real-model experiment passed on H200 and failed all three subcases with the guard removed (96 != 64); both Python and Rust shared-suite -k swa_branch runs pass (76 discovered, 5 executed, 71 skipped per backend); pre-commit passed for the guard and experiment. The experiment is retained as reproduction evidence in #38815; this PR does not add it to permanent CI or change the Inkling CI time estimate. The broader historical suite and DSV4 results above predate this guard.


CI States

Latest PR Test (Base): ✅ Run #34507885445
Latest PR Test (Extra): ❌ Run #34507885297
Latest PR Test (AMD ROCm 10): ❌ Run #34507885442

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T06:34:46.777023Z 58a9d2f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58a9d2fd8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rust/sglang-radix-tree/src/components/swa.rs Outdated
@alphabetc1
alphabetc1 force-pushed the feat/rust-swa-branching branch from 58a9d2f to be0fbe9 Compare September 5, 2026 07:30
Comment thread python/sglang/srt/mem_cache/unified_cache/components/swa_component.py Outdated
Comment thread rust/sglang-radix-tree/src/components/swa.rs Outdated
Comment thread rust/sglang-radix-tree/src/components/swa.rs Outdated
@alphabetc1
alphabetc1 force-pushed the feat/rust-swa-branching branch from be0fbe9 to d6d7aea Compare September 5, 2026 10:04
@github-actions github-actions Bot added Multi-modal multi-modal language model deepseek labels Sep 7, 2026
@alphabetc1

Copy link
Copy Markdown
Collaborator Author

/rerun-test registered/unit/mem_cache/test_rust_tree_core_integration.py
/rerun-test registered/unit/mem_cache/test_unified_radix_cache_unittest.py
/rerun-test registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_swa.py

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test registered/unit/mem_cache/test_rust_tree_core_integration.py:

🚀 ubuntu-latest (1 test): ✅ View workflow run

cd test/ && python3 registered/unit/mem_cache/test_rust_tree_core_integration.py

@alphabetc1

Copy link
Copy Markdown
Collaborator Author

/rerun-test -c

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test -c:

🚀 ubuntu-latest (1 test): ✅ View workflow run

cd test/ && python3 registered/unit/mem_cache/test_rust_tree_core_integration.py

🚀 1-gpu-5090 (1 test): ✅ View workflow run

cd test/ && python3 registered/unit/mem_cache/test_unified_radix_cache_unittest.py

Comment thread rust/sglang-radix-tree/src/components/swa.rs Outdated
Comment thread python/sglang/srt/mem_cache/unified_cache/components/swa_component.py Outdated

@ispobock ispobock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next todo is to align the swa checkpoint with the mamba checkpoint

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] SWA branching attaches a later Mamba checkpoint to an earlier prefix

3 participants