Skip to content

fix: emit SGLang-mode KV events at SGLang's insert points and cadence - #410

Closed
PeaBrane wants to merge 1 commit into
mainfrom
rupei/sglang-kv-event-cadence
Closed

PeaBrane wants to merge 1 commit into
mainfrom
rupei/sglang-kv-event-cadence

Conversation

@PeaBrane

@PeaBrane PeaBrane commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Summary

SGLang mode published KV events at the wrong points and far too often. It published a Stored event as soon as it allocated prompt pages, before the prompt was computed. It also inserted each decode page into the radix tree as soon as the page filled. At page size 1, a request therefore produced about one Stored event per output token, plus one Removed event for every decode step that evicted. That is roughly 50 times more events than a real SGLang worker sends to a KV-aware router.

This PR changes SGLang mode to behave like SGLang v0.5.21:

  • Insert points. The prompt is inserted after the prefill forward (cache_unfinished_req, once per chunk for chunked prefill). This happens even when the first decode slots cannot be reserved, and for a disaggregated prefill whose KV is still transferring. The rest of the sequence is inserted when the finished request is released (release_kv_cache). Cancelling a running request finishes it the same way, as an SGLang abort does. Retraction releases KV that was never inserted without inserting it (release_kv_cache(is_insert=False)).
  • Event queue. Events are queued the way SGLang's tree-cache event queue does it. A removal merges into a removal at the tail of the queue. A store whose parent is the last block of the tail store extends that store. Event IDs are assigned at publish time and stay dense.
  • Publish cadence. The queue is flushed after every prefill pass and every --decode-log-interval (40) decode passes. It is also flushed when the scheduler is idle, including after a command when no request is running.

Behavior changes (SGLang mode only)

  • Events. KV events are fewer and larger. They are published at the next publish point, which can be up to 40 decode passes later, instead of immediately. Stored and removed block totals are unchanged. Pages that never enter the radix tree, such as in-flight decode pages, are no longer announced as stored or removed.
  • Routing. KV-aware routing sees a request's decoded blocks only when the request finishes or is cancelled, as with SGLang.
  • Prefill insert. The post-prefill insert covers only the prompt. The block holding the first output token is stored when the request finishes.
  • Disaggregated prefill. A prefill source's prompt is inserted and published after its prefill pass, so it is matchable while its KV transfers. Before, it was published when its pages were allocated and entered the radix tree only when the source was released.
  • Prefix matching. The simulator's own radix cache can no longer match decoded tokens of in-flight requests.
  • Retraction. A retracted request discards its decoded KV and recomputes it when it is readmitted. Only its prompt stays cached. This can also change LPM ordering for retracted requests.
  • Timing. Simulated TTFT and throughput can therefore change under KV pressure. On the workloads below, total simulated run time was identical before and after.
  • Other engines. vLLM and TRT-LLM modes are unchanged.

Measured cadence

Real SGLang setup:

  • SGLang 0.5.21, Qwen3-0.6B, --page-size 1, radix cache, chunked prefill 8192, ZMQ KV-event publisher.
  • Workload: a multi-turn coding-agent workload sent by closed-loop lanes. Prompt token counts were scaled down by 8 to fit a 40,960-token context.

Simulator setup:

  • Each lane's request schedule was replayed offline through one aggregated SGLang-mode simulator worker, using Dynamo's mocker.
  • "Before" is the aisimulate-core release that Dynamo currently pins (0.13.0-dev.202609300000000061), unmodified. "After" is that release with this change backported.

Page size 1, 16 lanes, 393,216-token KV, 6,262 requests

Source Stored events/request Blocks per Stored event (median) Removed events/request Blocks per Removed event (median)
Real SGLang 2.00 370 (98) 0.49 1,375 (472)
Simulator before 112.2 6.6 (1) 11.4 59 (16)
Simulator after 2.00 371 (98) 1.98 341 (112)

Page size 1, 4 lanes, 98,304-token KV, 6,258 requests

Source Stored events/request Blocks per Stored event (median) Removed events/request Blocks per Removed event (median)
Real SGLang 2.00 383 (98) 0.55 1,373 (408)
Simulator before 112.2 6.8 (1) 29.9 25 (4)
Simulator after 2.00 384 (98) 2.63 285 (80)

Page size 16 (simulator only; there is no real SGLang capture at this page size)

Run Stored events/request (before → after) Blocks per Stored event (before → after) Removed events/request (before → after)
16 lanes 7.72 → 1.93 6.0 → 23.9 5.69 → 1.85
4 lanes 7.72 → 1.93 6.2 → 24.7 7.47 → 2.36

In all eight simulator runs, stored and removed block totals and total simulated run time were identical before and after.

Known follow-up

Removed events are still about four times too frequent:

  • 16 lanes: 1.98 vs 0.49 per request, at 341 vs 1,375 blocks per event.
  • 4 lanes: 2.63 vs 0.55 per request.

The cause is eviction granularity. SGLang evicts whole radix leaves, while this cache evicts only the pages it needs. In a local experiment, evicting whole leaves brought the simulator to 0.47 Removed events per request at 1,437 blocks per event. That change alters cache retention, and with it hit rates and timing, so it is not part of this PR.

#363 moves SGLang mode onto SGLang's native radix core with whole-leaf LRU eviction, so it may close this gap. The two PRs touch the same files, and whichever lands second will need a rebase.

Validation

  • cargo test --workspace: 2,057 passed, 1 ignored.
  • cargo fmt --all -- --check: passed.
  • cargo clippy --workspace --all-targets -- -D warnings (Rust 1.96.1): reports the same 158 existing findings as main. This PR adds none.
  • Regression tests, each failing without its fix:
    • prompt_is_cached_after_prefill_when_first_output_slots_do_not_fit: after a prefill whose first decode slots do not fit, the prompts are matchable, their Stored events are queued, and a request retracted at the next decode step keeps its prompt cached.
    • held_source_prompt_is_cached_and_published_after_prefill: a disaggregated prefill source's prompt is published with its prefill pass and stays matchable while the source is held; releasing it publishes nothing more.
    • cancelled_running_request_caches_its_decoded_tokens: cancelling a decoding request caches and publishes its full decoded pages.
  • kv_events_store_prompt_after_prefill_and_outputs_at_finish (page sizes 1 and 4) and kv_event_removals_are_batched_like_sglang fix the expected events for each pass. Existing KV-manager tests now flush the event queue where they read events.
  • Cadence replay: the eight simulator runs above. The "before" runs reproduce their earlier recorded results exactly. The "after" runs were measured before the fixes for cancellation and for requests that finish with their first token. Those fixes do not affect these replays, which are aggregated and cancel nothing. On a synthetic aggregated workload at page sizes 1 and 4, with shared prefixes, KV pressure, retractions and 3 in 7 requests finishing with their first token, the published KV events, output tokens and pass end times were identical with and without those fixes.
  • Not run: crates/tests/public-api. This PR changes no public API.

The SGLang scheduler published a Stored event when it allocated prompt
pages, and it inserted each decode page into the radix tree as soon as the
page filled. At page size 1 a request therefore produced about one Stored
event per output token, plus one Removed event for every decode step that
evicted. SGLang v0.5.21 behaves differently:

- it inserts a request's prompt into its radix tree after the prefill
  forward (`cache_unfinished_req`, once per chunk for chunked prefill).
  It does so whether or not the request's first decode slot then fits,
  and for a disaggregated prefill while its KV transfers. It inserts the
  rest when it releases the finished request (`release_kv_cache`),
  including a running request that an abort finished. Decoded tokens are
  neither matchable nor published before then, and retraction releases
  them without inserting them (`release_kv_cache(is_insert=False)`);
- it queues events in the tree cache. A removal merges into a removal at
  the tail of the queue, and a store whose parent is the last block of the
  tail store extends that store;
- it publishes the queue after every prefill batch, after every
  `--decode-log-interval` (default 40) decode batches, and when idle.

The SGLang KV manager and scheduler now do the same. Allocation no longer
publishes, and decode no longer inserts per token. The post-prefill insert
covers the prompt only. It runs even when the first decode slots cannot be
reserved, and also for requests that finish with their first token, which
include disaggregated prefill sources. Cancelling a running request
finishes it, so the KV it decoded is inserted. Events go through a queue
that merges like SGLang's and assigns event IDs at publish time, and the
queue is flushed at SGLang's publish points. Commands flush the queue when
no request is running.

Behavior changes in SGLang mode:

- KV events are fewer and larger, and are published at the next publish
  point (up to 40 decode passes later) instead of immediately. Stored and
  removed block totals are unchanged. Pages that never enter the radix
  tree are no longer announced as stored or removed.
- KV-aware routing sees a request's decoded blocks only when the request
  finishes or is cancelled, as with SGLang.
- A disaggregated prefill source's prompt is inserted and published after
  its prefill pass. Before, it was published when its pages were
  allocated, and it entered the radix tree only when the source was
  released.
- Decoded tokens of in-flight requests are no longer prefix-matchable by
  the simulator's own radix cache.
- A retracted request discards its decoded KV and recomputes it when it
  is readmitted; only its prompt stays cached.
- Simulated TTFT and throughput can therefore change under KV pressure.

vLLM and TRT-LLM modes are unchanged.

We recorded a multi-turn coding-agent workload on SGLang 0.5.21 at page
size 1 (16 closed-loop lanes, 6,262 requests). Real SGLang emitted 2.00
Stored events per request at 370 blocks per event (median 98), and 0.49
Removed events per request at 1,375 blocks per event. Replaying the same
schedule, the simulator went from 112.2 to 2.00 Stored events per request
(6.6 to 371 blocks per event, median 98), and from 11.4 to 1.98 Removed
events per request (59 to 341 blocks per event). A 4-lane run with a
quarter of the KV capacity agrees on stores (real and simulated: 2.00 per
request at 383 and 384 blocks per event). At page size 16, Stored events
per request fell from 7.7 to 1.9.

The removal gap that remains comes from eviction granularity. SGLang
evicts whole radix leaves, while this cache evicts only the pages it
needs. Evicting whole leaves in a local experiment brought the simulator
to 0.47 Removed events per request at 1,437 blocks per event. That change
alters cache retention, so it is left for a follow-up.

Signed-off-by: PeaBrane <yanrpei@gmail.com>
@PeaBrane
PeaBrane requested review from a team as code owners October 7, 2026 17:13
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: ai-dynamo/aisimulate/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Enterprise
  • Run ID: f4a8308b-6761-43f1-99db-a0cc0c306fcb
📥 Commits

Reviewing files that changed from the base of the PR and between e45612e and ac9111d.

📒 Files selected for processing (4)
  • crates/core/src/engine/kv_manager/sglang_backend.rs
  • crates/core/src/engine/scheduler/sglang/core.rs
  • crates/core/src/engine/scheduler/sglang/decode.rs
  • crates/core/src/engine/scheduler/sglang/tests.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (25)
  • GitHub Check: Prediction Regression / Collect prediction snapshot (old)
  • GitHub Check: Prediction Regression / Collect prediction snapshot (new)
  • GitHub Check: Collector Data / Perf data sanity (informational)
  • GitHub Check: Platform Wheels / Build wheels (macosx_arm64)
  • GitHub Check: Platform Wheels / Build wheels (manylinux_2_28_aarch64)
  • GitHub Check: Collector Data / Check collector data
  • GitHub Check: Platform Wheels / Build wheels (manylinux_2_28_x86_64)
  • GitHub Check: readme-commands / README / dynamo
  • GitHub Check: readme-commands / README / source
  • GitHub Check: Python 3.11 compatibility
  • GitHub Check: Application Test Wheel (arm64)
  • GitHub Check: Python 3.13 compatibility
  • GitHub Check: Release Artifact Contract (arm64)
  • GitHub Check: Public API Rust (arm64)
  • GitHub Check: Engine Golden Regression
  • GitHub Check: Application Test Wheel (amd64)
  • GitHub Check: Release Artifact Contract (amd64)
  • GitHub Check: Public API Rust (amd64)
  • GitHub Check: Rust feature modes
  • GitHub Check: Python Dependency Licenses
  • GitHub Check: Rust (arm64)
  • GitHub Check: Rust (amd64)
  • GitHub Check: Build simulation wheel (head)
  • GitHub Check: Build simulation wheel (base)
  • GitHub Check: Forward Prediction Performance (advisory)
🧰 Additional context used
📚 Code guidelines (2)
AGENTS.md — configured
REVIEW.md — configured
📓 Path-based instructions (4)
Review scheduler state transitions and event ordering, including cancel, preempt, drain, retry, duplicate, empty, and terminal paths.

⚙️ CodeRabbit configuration file

Files:

  • crates/core/src/engine/scheduler/sglang/decode.rs
  • crates/core/src/engine/scheduler/sglang/core.rs
  • crates/core/src/engine/scheduler/sglang/tests.rs
  • crates/core/src/engine/kv_manager/sglang_backend.rs
Read REVIEW.md before commenting.

⚙️ CodeRabbit configuration file

Files:

  • crates/core/src/engine/scheduler/sglang/decode.rs
  • crates/core/src/engine/scheduler/sglang/core.rs
  • crates/core/src/engine/scheduler/sglang/tests.rs
  • crates/core/src/engine/kv_manager/sglang_backend.rs
Before making any change under: `python/aisimulate/src/aisimulate/generator/**` MUST read: `python/aisimulate/.claude/rules/generator-development.md` Source excerpt: Before making any change under `python/aisimulate/collector/**` MUST read:...

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • crates/core/src/engine/scheduler/sglang/decode.rs
  • crates/core/src/engine/scheduler/sglang/core.rs
  • crates/core/src/engine/scheduler/sglang/tests.rs
  • crates/core/src/engine/kv_manager/sglang_backend.rs
Source excerpt: Only workflows under the repository-root `.github/workflows/` run for this repository.

📄 CodeRabbit inference engine (REVIEW.md)

Files:

  • crates/core/src/engine/scheduler/sglang/decode.rs
  • crates/core/src/engine/scheduler/sglang/core.rs
  • crates/core/src/engine/scheduler/sglang/tests.rs
  • crates/core/src/engine/kv_manager/sglang_backend.rs
🔀 Multi-repo context ai-dynamo/dynamo, ai-dynamo/aiconfigurator

Linked repositories findings

ai-dynamo/dynamo

  • The downstream wire contract is a batch [timestamp, events, dp_rank]; stored events require block hashes, parent hash, token IDs, and block size, while removed events carry block hashes. [::ai-dynamo/dynamo::] (lib/kv-router/src/zmq_wire/types.rs:12-29,93-152)
  • Dynamo documents SGLang as a ZMQ-relay producer. Event IDs are assigned internally by the publisher and must be monotonic per worker; raw ZMQ events do not contain event IDs. [::ai-dynamo/dynamo::] (docs/.../publish-kv-events.md:31-58,145-210,261-300)
  • The unified backend requires stable DP-rank ownership and a matching KV block size for event publication. [::ai-dynamo/dynamo::] (components/src/dynamo/common/backend/README.md:311-344)

ai-dynamo/aiconfigurator

  • SGLang router deployments emit --kv-events-config on every aggregate, prefill, and decode worker, using the kv-events topic and one incremented port per worker. [::ai-dynamo/aiconfigurator::] (src/aiconfigurator/generator/config/backend_templates/sglang/run.sh.j2:20-24,43-60,76-93,110-123)
  • Golden tests require SGLang KV routing to retain this configuration and default port 5557; no migration of the existing configuration contract is indicated. [::ai-dynamo/aiconfigurator::] (tests/golden/generator/test_release_1_2_0_backend_contracts.py:371-392)
  • Kubernetes generation likewise emits the ZMQ event configuration when routing is enabled. [::ai-dynamo/aiconfigurator::] (src/aiconfigurator/generator/builders/k8s_builder.py:643-648)
🔇 Additional comments (5)
crates/core/src/engine/scheduler/sglang/core.rs (2)

628-636: Avoid inserting a cancelled running request's decoded tokens when its lease is mid-prefill.

was_running covers only running requests. Chunked continuations are pushed back to waiting and use abort, so the change matches SGLang semantics here. Retracted requests are also in waiting, and they keep the abort path. No defect.


1155-1163: LGTM!

crates/core/src/engine/kv_manager/sglang_backend.rs (1)

1222-1246: LGTM!

crates/core/src/engine/scheduler/sglang/tests.rs (1)

1459-1482: LGTM!

crates/core/src/engine/scheduler/sglang/decode.rs (1)

299-308: LGTM!


📝 Summary

Risk level: Medium. Three areas need human attention:

  1. Confirm that prompt pages publish after prefill and decoded pages publish only when a request finishes or is cancelled.
  2. Confirm the flush cadence: after prefill passes, every 40 decode forwards, and when the scheduler is idle.
  3. Confirm that cancellation and retraction preserve the intended KV ownership and event behavior.

Behavior and contracts

SGLang mode now queues KV events and publishes them at SGLang insert points. flush_events assigns event IDs in publish order and batches compatible events. The scheduler controls when the queue flushes. vLLM and TRT-LLM are reported unchanged. The listed API change is pub(crate), so the supplied change summary identifies no public API change.

Evidence supplied

The PR objectives report that workspace tests passed (2,057 passed, 1 ignored) and formatting checks passed. Clippy reported 158 existing findings, with none added. Named regression tests cover prefill caching, held disaggregated prefill sources, cancellation, event placement, and batched removals. The author also reports cadence replays and a synthetic workload check.

The objectives report fewer Stored and Removed events in a 16-lane, page-size-1 replay, with unchanged stored and removed block totals and simulated run time across eight runs. They also report that TTFT and throughput can change under KV pressure. These are author-reported results; no independent measurements are supplied here.

Evidence missing

No current review findings or severity counts were supplied. The actual test output, replay data, and source diff were not supplied for independent verification. crates/tests/public-api was not run, according to the objectives.

Technical quality and merge readiness

The supplied test and replay claims support the intended behavior, but they do not establish independent verification. Assess merge readiness against the full diff and test evidence, with particular attention to flush timing and cancellation/retraction behavior. Do not treat automated review as approval.

Walkthrough

SGLang KV events are queued and published at scheduler-controlled points. The scheduler now handles running-request cancellation through completed-request cleanup. Prefill caches prompt prefixes before decode-page reservation, while generated tokens enter the radix tree when a request finishes or is cancelled.

Changes

SGLang KV event flow

Layer / File(s) Summary
Queue and publish KV events
crates/core/src/engine/kv_manager/sglang_backend.rs
The manager queues and merges compatible stored and removed events, then assigns event IDs and publishes them on flush. Tests now flush explicitly and cover publication, merging, and admission behavior.
Schedule flushes and clean up cancellations
crates/core/src/engine/scheduler/sglang/core.rs, crates/core/src/engine/scheduler/sglang/tests.rs
The core flushes events when idle, after non-decode passes, and every 40 decode forwards. Running-request cancellation uses completed-request cleanup. Tests cover flush timing and handoff publication.
Cache prompts and completed generated tokens
crates/core/src/engine/scheduler/sglang/decode.rs, crates/core/src/engine/scheduler/sglang/tests.rs
Prefill caches materialized prompt prefixes before decode-page reservation. Generated tokens remain outside the radix tree until request completion or cancellation. Tests cover cache retention and insertion timing.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to ac911

No actionable issue remains from this review; the change is mergeable after normal checks.

🚥 Pre-merge checks | ✅ 6 | ❌ 1 | ❓ 1

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cross-Layer Contract ⚠️ Warning The PR adds a new compatible-Stored queue merge path in merge_into_tail (crates/core/src/engine/kv_manager/sglang_backend.rs:1224-1246), but no test asserts that adjacent stored events merge. Th… Add a focused backend test that queues two adjacent Stored events where the second event's parent_hash equals the first event's last block_hash, then flushes and asserts one event with all blocks, the correct parent, and dense event I…
Review Evidence ❓ Inconclusive The supplied PR description is truncated before the validation section. The available metadata reports cargo test --workspace, cargo fmt --all -- --check, and `cargo clippy --workspace --all-targe… Provide the complete PR description, including the validation commands and results and any hosted-CI or workflow references.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title precisely describes the behavioral change: SGLang-mode KV events are emitted at SGLang insert points and publish cadence. The fix: prefix does not make the title vague.
Description check ✅ Passed The description clearly explains the problem, behavior changes, affected scope, measured results, validation commands, known limitation, and follow-up work. It omits the template's explicit Review map…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Modeling And Data Evidence ✅ Passed The PR provides provenance and matched before/after evidence for the changed simulated outputs. The commit records the SGLang v0.5.21 oracle, workload size, page size, lane count, request count, and r…
Compatibility Boundaries ✅ Passed No compatibility boundary is crossed. The PR changes only four internal Rust SGLang files. It adds a pub(crate) flush method and keeps the existing KvEvent fields and Stored/Removed payload sc…
Full details: Cross-Layer Contract

Explanation

The PR adds a new compatible-Stored queue merge path in merge_into_tail (crates/core/src/engine/kv_manager/sglang_backend.rs:1224-1246), but no test asserts that adjacent stored events merge. The scheduler tests flush between the prompt and completion stores (crates/core/src/engine/scheduler/sglang/tests.rs:1451-1455), and the backend tests also flush before the second store (crates/core/src/engine/kv_manager/sglang_backend.rs:1937-1946). The removal batching path is tested, but the stored batching path is not. This violates the check's explicit requirement that a new path is tested.

Resolution

Add a focused backend test that queues two adjacent Stored events where the second event's parent_hash equals the first event's last block_hash, then flushes and asserts one event with all blocks, the correct parent, and dense event IDs. Add an end-to-end scheduler test that reaches this merge through consecutive compatible insertions without an intervening flush. Also assert that incompatible stored events remain separate.

Full details: Review Evidence

Explanation

The supplied PR description is truncated before the validation section. The available metadata reports cargo test --workspace, cargo fmt --all -- --check, and cargo clippy --workspace --all-targets -- -D warnings, plus boundary and parity evidence, but that report is not the authored description. The commit body confirms real SGLang, simulator, local, and parity measurements, and contains no inactive nested workflow claim. However, the full description is unavailable, so the required description-specific evidence and absence of omitted hosted-CI claims cannot be verified.

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@PeaBrane

Copy link
Copy Markdown
Contributor Author

Superseded by #583, which now contains this change together with the related SGLang cache lifecycle, eviction, reporting, and KV-event corrections. The consolidated branch has passed local core and public-API validation.

@PeaBrane PeaBrane closed this Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant