Repository navigation
Conversation
qidaye
requested review from
Ying1123,
alphabetc1,
hanming-lu,
hnyls2002,
huangtingwei9988,
hzh0425,
ispobock,
merrymercy,
xiezhq-hermann and
yizhang2077
as code owners
September 8, 2026 09:11
start_loading submits the whole host->device burst inline on the scheduler thread. With io_backend='direct' every per-layer submission builds a batched-copy descriptor on the CPU, so a burst on a deep model keeps the scheduler busy for hundreds of milliseconds and every copy has landed before the forward launches; the layer-wise pipeline overlaps almost nothing. SGLANG_HICACHE_ASYNC_LOAD_ENQUEUE (default off) keeps the producer-slot hand-out and start_event.record() on the scheduler thread and hands the index move, the per-layer enqueue and the ack to a single FIFO daemon thread. To keep that correct: - LayerLoadingEvent grows a CPU-side per-layer "recorded" flag that wait() blocks on, since waiting on an unrecorded device event is a no-op and a consumer would sail past a layer not yet submitted. - update_producer waits for the slot's previous burst to be enqueued before checking finish_event, which is otherwise the stale event of the previous rotation. - The loader runs the index move under the H2D stream after waiting on start_event, because op.device_indices is written by the allocator on the compute stream. - The overlap-scheduling fence is captured as an event on load_fence_stream at hand-over time and waited on by the loader. - A loader failure completes every layer event on the H2D stream and publishes a zero-token ack so no forward or later ack can hang; the error is logged and latched in load_enqueue_error. - reset() drains the loader before clearing ack_load_queue, and the ack append is guarded by a lock against that clear. - is_load_back_event_done ignores a slot whose burst is still queued. - HybridCacheController carries the flag into its replacement counter. With the gate off the call sequence is unchanged (covered by the test).
qidaye
force-pushed
the
feat/hicache-async-load-enqueue
branch
from
September 30, 2026 07:44
9965ce4 to
4a07e45
Compare
This was referenced Oct 1, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Closes #38470
HiCacheController.start_loadingsubmits the whole host->device load-back burst inline on the scheduler thread. With--hicache-io-backend directand thepage_first_directlayout every per-layerload_to_device_per_layerbuilds a batched-copy descriptor on the CPU (~2.4 ms per layer in our profile), so one burst on a 78-layer model with two host pools keeps the scheduler thread busy for ~375 ms. By the time the batch is built and the forward launches, every copy has already landed and the layer-wise pipeline (LayerDoneCounter.wait_untilin the attention layers) overlaps ~0.04 % of the copy time.This PR adds
SGLANG_HICACHE_ASYNC_LOAD_ENQUEUE(default off). With it on,start_loadingonly does the producer-slot hand-out andstart_event.record()on the scheduler thread, then hands the index move, the per-layer enqueue and the ack to a single FIFO daemon thread (hicache-load-enqueue). With it off the call sequence is byte-for-byte the previous one (the unit test asserts the exact stream/event sequence).Modifications
python/sglang/srt/managers/cache_controller.pyLayerLoadingEvent(num_layers, async_enqueue=False): per-layer CPU-siderecordedflags. Waiting on a device event that was never recorded is a no-op, sowait()first blocks on the flag (logging every 30 s if it is still unset), then waits on the device event. A per-slotenqueue_doneflag is the rotation's backpressure:update_producerwaits on it before checkingfinish_event, because while a burst is still queued the slot'sfinish_eventis the previous rotation's andquery()would pass.rearm(),is_enqueue_done(),mark_enqueue_done(),disable_async_handshake().HiCacheController:_init_load_enqueue_thread,_load_enqueue_thread_func(single consumer; the H2D stream ordering, the 3-slot rotation andack_load_queuepopped in order are all FIFO),_abort_load_burst,_drain_load_enqueue,stop_load_enqueue_thread(joins, then falls back to the inline path and drops the handshake). Startup logsHiCache async load enqueue: enabled/disabled (<ClassName>).start_loadingsplit into the scheduler-thread half and_enqueue_load_burst(op, producer_event, start_event_recorded, fence_event):start_event.record(), fence, submit, ack, exactly as before;op.device_indicesis written by the allocator on the compute stream, so the index move runs under the H2D stream afterstart_event.wait(h2d_stream); the overlap-scheduling fence is captured on the scheduler thread as an event recorded onload_fence_streamat hand-over time and waited on by the loader before submitting.wait_untiland the later acks behind this one cannot hang. The error is logged with the node ids and latched inload_enqueue_error; the KV for those nodes is stale.reset()drains the loader first, and_append_load_acktakes_ack_load_lockso a loader-thread append cannot land afterreset()'sclear().python/sglang/srt/mem_cache/hybrid_cache/hybrid_cache_controller.py: the replacementLayerDoneCounterbuilt fortransfer_layer_id_maxcarriesasync_enqueue.python/sglang/srt/mem_cache/unified_radix_cache.py:is_load_back_event_donereturns False while the slot's burst is still queued (thegetattrkeeps the FlexKV counter, which has its own event class, untouched).python/sglang/srt/environ.py:SGLANG_HICACHE_ASYNC_LOAD_ENQUEUE = EnvBool(False).Tests
test/registered/unit/mem_cache/test_hicache_async_load_enqueue.py(CPU,base-a-test-cpu): swaps thedevice_moduleof the controller and ofL2TransferEnginefor a fake that logs the exact stream/event call sequence with the calling thread. Covers: gate off creates no thread and keeps the pre-split call sequence (including the fencewait_stream); producer hand-out is synchronous and the enqueue is not; loader binds to the scheduler's device; ack only after the last layer; every layer completes so a consumer cannot hang; bursts complete in submission order; slot reuse blocks on a queued burst;reset()does not resurrect an ack; the fence event is recorded at hand-over and waited on the loader; the index move runs under the H2D stream after thestart_eventwait (with a sentinel that returns garbage if read early); loader failure logs, releases the consumer and still acks, and the next burst is served; shutdown joins and falls back to inline;is_load_back_event_doneguard; hybrid burst with a sidecar pool inline and on the loader thread; activation log line andtransfer_layer_id_maxcounter carry-over.test_hicache_staged_write_back_dispatch.py::test_hybrid_load_forwards_merged_pool_transfersbuilds its controller withmock.Mock(spec=...); it now wires the inline burst body so the split does not turn it into a no-op.Accuracy Tests
No numerical change: the same copies are issued on the same stream, only the thread that issues them changes. Gate off is the existing path.
Speed Tests and Profiling
Prefill-only H100 TP=8 deployment, hierarchical cache,
io_backend=direct,page_first_direct, 500 requests, same prompt set, gate off vs on:Per-burst
start_loadingon the scheduler thread drops from ~375 ms to the slot hand-out.Checklist
CI States
Latest PR Test (Base): ❌ Run #34208615291
Latest PR Test (Extra): ❌ Run #34208615111
Latest PR Test (AMD ROCm 7.2): ❌ Run #34208615213