Repository navigation
fix: per-layer store copy, test plugin isolation, async-load block budget - #96
Draft
youngrok-XCENA wants to merge 5 commits into
Draft
youngrok-XCENA wants to merge 5 commits into
youngrok-XCENA wants to merge 5 commits into
Conversation
The per-layer packed-store fallback (used by CPU and mixed mode, and by CXL when the maru_kv_ops kernel is unavailable) copied a layer's [2, tokens, hidden] extract into slab[:, layer] in one copy_. That destination is not contiguous: other layers sit between the layer's K and V planes. Measured on its own (save_kv_layer over 24 layers, Qwen2.5-0.5B geometry, 12 chunks, one RTX PRO 6000, pageable host buffer), the store took 1.73-1.81 s, about 6 ms per (chunk, layer). Each plane slab[k, layer] is contiguous, so two copies per layer write the same bytes in about 21 ms in total.
youngrok-XCENA
force-pushed
the
fix/lease-path-store-copy-sync-scheduling
branch
from
October 2, 2026 10:03
a137326 to
5e89c8a
Compare
MaruHandler loads every package registered under maru.handler_plugins. When a vendor plugin is installed in the test environment, every handler built by a test loads it, and the plugin sends its hints to the DAX path the unit mock reports (/dev/dax0.0), which can be a real device in use by another job. The plugin loader tests also read MARU_PLUGINS from the developer's shell, so exporting it (for example to disable plugins) made four of them fail. An autouse fixture now empties entry-point discovery and clears MARU_PLUGINS for every test. Loader tests keep passing their own entry points.
youngrok-XCENA
added a commit
that referenced
this pull request
Oct 7, 2026
…e-kv-backend Both sides changed MaruServer.request_alloc and its CLI: keep the remote access options and managed owners next to the client leases. Keep this branch's names in the vLLM connector (_lease_mode, _unkeyed_requests).
2 of 4 tasks
A request parked for an async load holds blocks for its loaded prefix only, and resumes only once its whole prompt fits. vLLM checks that fit for each request against the blocks free when it parks, so parked requests can together hold the cache while the oldest still needs more. With none of them running, no block is ever freed and the engine stops until clients abort requests. The scheduler side now tracks the blocks each parked request needs for its whole prompt and asks vLLM to retry a request later (None from get_num_new_matched_tokens) when parking it would overcommit the cache. Requests told to wait keep their place ahead of later ones, so a long prompt is not passed over.
vLLM keeps one GPU KV block aside as its null block, so requests can hold one block fewer than kv_cache_config.num_blocks. Size the budget on the blocks requests can actually hold.
youngrok-XCENA
force-pushed
the
fix/lease-path-store-copy-sync-scheduling
branch
from
October 12, 2026 04:30
ce95214 to
4c7e6a6
Compare
youngrok-XCENA
added a commit
that referenced
this pull request
Oct 12, 2026
…kv-backend #96 no longer carries the client lease commit; its other changes are already here, so the merge changes no file.
youngrok-XCENA
added a commit
that referenced
this pull request
Oct 12, 2026
#96 removed its client lease commit (aac1426): reclaiming a region at lease expiry also reclaims the regions of a live handler that only stalled, and the handler's late write can land in KV that another handler stored in the reused range. This branch got the lease through the #96 merge; remove it here too. The managed-owner checks for remote access stay.
…room vLLM asks a request that the parked-load budget told to wait again on every step. Each ask ran the prefix match first, which sends an existence check to MaruServer (every time in CPU mode, and in CXL mode unless every chunk is a known key) and logs a "Maru KV hit" line, once per waiting request per step. A request already waiting now checks the budget first and answers from it alone until the budget has room; then the usual lookup runs. A waiter whose prefix disappeared meanwhile computes once it reaches the front instead of on the next step. The async-load docs and the maru_async_load docstring now describe the budget.
youngrok-XCENA
added a commit
that referenced
this pull request
Oct 12, 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.
🤔 Background & Motivation (Why)
이 PR 은 서로 독립된 결함 세 가지를 고칩니다. 결함마다 커밋이 있어 커밋 단위로 리뷰할 수 있습니다.
maru_async_load에서 적재를 기다리는 요청들이 GPU KV 블록을 나눠 쥐어, 가장 먼저 온 요청이 나머지 블록을 받지 못함1. 층별 저장 복사가 느림
CPU·mixed 모드(#88)에서 KV 를 저장하는 층별 복사가 비연속 목적지에 한 번에 씁니다. 이 복사는 torch CPU 스레드 수가 많으면 크게 느려집니다.
[K·V(2), 층, 토큰, hidden]순서로 배치됩니다. 그래서 한 층의 K 평면과 V 평면 사이에는 다른 층들이 끼어 있습니다.maru_kv_ops커널을 쓰지 못할 때 씁니다.2. 단위 시험이 설치된 장치 플러그인을 불러옴
시험 환경에 장치 벤더 플러그인이 설치되어 있으면, 단위 시험이 만든 핸들러가 그 플러그인을 불러 실제 DAX 장치에 힌트를 보냅니다.
MaruHandler는 생성될 때maru.handler_plugins엔트리포인트에 등록된 플러그인을 모두 불러옵니다./dev/dax0.0으로 알려 줍니다. 플러그인은 이 경로를 실제 장치로 보고 힌트를 보내며, 그 장치는 같은 호스트의 다른 작업이 쓰고 있을 수 있습니다.MARU_PLUGINS값도 시험에 그대로 들어갑니다. 예를 들어 플러그인을 끄려고MARU_PLUGINS=none을 내보내면 플러그인 로더 시험 4개가 실패합니다.3. 비동기 적재 대기 요청들이 GPU KV 를 모두 쥐고 멈춤
maru_async_load를 켜면, 적재가 오래 걸릴 때 vLLM 엔진이 아무 요청도 처리하지 않고 멈출 수 있습니다. 클라이언트가 요청을 끊어 블록이 풀려야 다시 움직입니다.WAITING_FOR_REMOTE_KVS)에 둡니다. 이때 블록은 적재할 접두사 몫만 잡습니다.scheduler_reserve_full_isl, 기본 켬). 요청마다 그 순간의 빈 블록과 비교하므로, 적재 대기 요청이 여럿 쌓이면 그 요청들이 다 함께 필요로 하는 블록은 GPU KV 전체를 넘을 수 있습니다.🏗️ Design Changes
1. 층별 저장 복사
한 층의 K 와 V 를 각각 연속 영역으로 두 번 복사합니다. 슬랩의 배치와 바이트는 그대로이고, 복사하는 단위만 바뀝니다.
flowchart LR subgraph before["Before: 층마다 복사 한 번"] g1["GPU 의 한 층 K·V"] -->|"복사 한 번, 비연속 목적지"| s1["슬랩의 그 층 자리<br/>K 평면과 V 평면이 떨어져 있음"] end subgraph after["After: 층마다 복사 두 번"] g2["GPU 의 한 층 K"] -->|"연속 복사"| k2["슬랩 K 평면의 그 층"] g3["GPU 의 한 층 V"] -->|"연속 복사"| v2["슬랩 V 평면의 그 층"] end2. 시험의 플러그인 격리
모든 시험에서 플러그인 엔트리포인트 탐색이 빈 목록을 돌려주고 플러그인 허용 목록 환경 변수가 비워집니다. 플러그인 로더 시험은 원래대로 자기 엔트리포인트를 직접 넘깁니다.
3. 비동기 적재 대기의 블록 예산
스케줄러 쪽 커넥터가 적재 대기 요청마다 프롬프트 전체에 필요한 블록 수를 기록합니다. 새 요청을 적재 대기로 받으면 그 합이 요청이 쥘 수 있는 GPU KV 블록 수를 넘게 되는 경우, vLLM 에 "다음 스텝에 다시 물어 달라"고 답합니다. 이렇게 하면 실행 중인 요청이 없어도 가장 먼저 온 대기 요청이 들어갈 자리가 늘 남습니다. 먼저 기다리게 한 요청의 몫은 뒤에 온 요청보다 먼저 잡아 두어, 긴 프롬프트가 계속 밀리지 않게 합니다. vLLM 은 기다리게 한 요청을 매 스텝 다시 묻는데, 예산에 자리가 날 때까지는 Maru 에 접두사를 다시 조회하지 않고 예산만 보고 답합니다.
flowchart TB subgraph before["Before"] b1["접두사가 있는 요청"] --> b2["그 순간 빈 블록에 프롬프트 전체가 들어가면 받음<br/>(vLLM 의 확인)"] b2 --> b3["적재 대기 요청이 쌓여<br/>다 함께 필요한 블록이 GPU KV 를 넘을 수 있음"] b3 --> b4["첫 요청이 나머지 블록을 못 받고 멈춤"] end subgraph after["After"] a1["접두사가 있는 요청"] --> a2{"적재 대기 요청들과<br/>먼저 기다린 요청들의 몫 + 이 요청<br/>≤ GPU KV 블록 수?"} a2 -->|"예"| a3["적재 대기로 받음"] a2 -->|"아니오"| a4["다음 스텝에 다시 물음<br/>(먼저 기다린 순서 유지,<br/>자리가 날 때까지 조회 없음)"] a3 --> a5["적재 뒤 첫 요청이 늘 들어감"] end📝 Implementation Details
1. 층별 저장 복사
maru_vllm/connector.pyMaruWorkerConnector._save_kv_layer_packed의 커널 없는 층별 복사:slab[:, layer_idx].copy_(kv_contig)를 평면마다slab[plane, layer_idx].copy_(kv_contig[plane])로 바꿨습니다.2. 시험의 플러그인 격리
tests/conftest.py에 autouse 픽스처_no_installed_handler_plugins를 추가했습니다.maru_handler.plugin._discover_entry_points를 빈 목록으로 바꾸고MARU_PLUGINS를 지웁니다.tests/unit/test_plugin_loader.py에 시험 안의 핸들러가 플러그인을 하나도 불러오지 않는지 보는 시험을 추가했습니다.3. 비동기 적재 대기의 블록 예산
maru_vllm/connector.py:MaruKVConnector가 스케줄러 쪽MaruSchedulerConnector에 요청이 쥘 수 있는 GPU 블록 수를 넘깁니다(_usable_gpu_blocks():kv_cache_config.num_blocks, 없으면cache_config.num_gpu_blocks에서 vLLM 의 null 블록 1개를 뺀 값, 4c7e6a6).get_num_new_matched_tokens()의 기존 일치 계산을_match()로 떼어 내고, 그 결과가 비동기 적재일 때_admit_parked()로 예산을 확인하고, 넘치면(None, False)를 돌려줍니다. vLLM 은 이 답을 받으면 그 요청을 다음 스텝에 다시 묻습니다._admit_parked()를 먼저 보고, 자리가 없으면_match()를 건너뛰고(None, False)를 돌려줍니다(b6f20fb). 비동기 적재는 CXL 모드에서만 켤 수 있습니다. 이 모드의_match()는 프롬프트의 청크 가운데 아직 확인하지 않은 키가 있으면 스케줄러 쪽 핸들러로 MaruServer 에 존재 확인을 보내고, 적중하면 "Maru KV hit" INFO 로그를 남깁니다. 그래서 이 변경 전에는 기다리는 요청마다 매 스텝 이 로그가 찍혔고, 프롬프트 뒷부분이 Maru 에 없는 요청은 존재 확인도 매 스텝 다시 보냈습니다._parked_blocks는 적재 대기 요청마다ceil(request.num_tokens / block_size)를 기록합니다.update_state_after_alloc()에서 적재 대기로 받을 때 넣고, 실행 블록을 받을 때(num_external_tokens <= 0)와 끝나거나 선점될 때(build_connector_meta정리) 뺍니다.get_num_new_matched_tokens()의 처음에서 자기 기록을 지웁니다._admission_waiters는 기다리게 한 요청을 먼저 온 순서로 기억하고, 그 몫을 뒤에 온 요청의 예산 계산에 더합니다. 앞에 아무도 없는 요청은 늘 받습니다(vLLM 은 최대 길이 요청 하나가 들어가게 GPU KV 를 잡습니다).docs/source/integration/vllm.md의 비동기 적재 절과maru_async_loaddocstring 에 예산을 적었습니다. 이 예산은 GPU 블록을 세고, 기존maru_load_admission_window는 동시에 진행하는 적재 수를 셉니다.처음 버전에서 뺀 변경
close()없이 끝난 핸들러의 영역을 서버가 회수하는 변경도 있었습니다. 실장치에서, 잠시 멈췄을 뿐 아직 살아 있는 핸들러의 영역도 이 변경이 회수해 다른 핸들러에게 내주었고, 멈췄던 핸들러가 깨어나 쓴 데이터가 새 핸들러의 KV 를 덮어썼습니다. 그래서 이 변경을 이 PR 에서 뺐고, 다른 설계로 따로 올립니다.✅ Tests
pytest tests/unit, GPU 를 숨기고MARU_PLUGINS=none인 환경)은 최종 커밋(b6f20fb)에서 통과 1,110개, 건너뜀 30개입니다.MARU_PLUGINS를 비우거나none·임의 이름으로 두어도 플러그인 로더 시험 18개가 모두 통과합니다.tests/unit/test_vllm_connector.py의TestDeferredAdmissionFitsCache(11개).make_vllm_scheduler)로 위 배경의 50·25·15블록 시나리오를 돌려, 적재가 끝난 뒤 첫 요청이 실행되는지 확인합니다. 이 변경 바로 앞(9d9486c)의 커넥터로 돌리면 실행된 토큰이 0 이라 실패합니다.(None, False), 먼저 기다린 순서 유지, 끝난 요청과 기다리다 끊긴 요청의 몫 해제, 적재 실패 뒤 다시 묻는 요청의 이전 기록 제거, 블록 수를 모를 때의 무제한을 확인합니다.ruff check와ruff format --check를 통과합니다.실장치 조건: RTX PRO 6000, vLLM 0.22.1, Qwen2.5-0.5B,
--enforce-eager --no-enable-prefix-caching,maru_kv_ops커널 없는 torch 복사 경로입니다. 비교한 main 은 e84ea9c(이 브랜치가 갈라진 9c0db55 와 문서만 다름)입니다. PR 쪽은 영역 회수 변경을 빼기 전 커밋(ce95214)으로 돌렸습니다. 뺀 변경은 서버·핸들러·RPC 코드만 바꿨고, 세 변경의 커넥터 코드는 b6f20fb 앞까지 지금과 같습니다. b6f20fb 는 실장치에서 다시 돌리지 않았습니다. 측정 호스트는 NVML 이 초기화되지 않아 vLLM 을 NVML 없이 쓰는 CUDA 플랫폼 경로로 띄웠습니다.1번, vLLM 서빙:
cpu백엔드(풀 8G, 청크 256,--no-async-scheduling)에 매번 새 3,304 토큰 프롬프트를 두 번씩 20회 보냈습니다. TTFT 는 스트리밍 클라이언트가 요청을 보낸 때부터 첫 토큰을 받은 때까지입니다.1번, 커넥터 단독: vLLM 없이 커넥터의 24층
save_kv_layer를 부르고 앞뒤로torch.cuda.synchronize()를 둔 벽시계 시간입니다. 목적지는 익명 mmap(CPU 모드 풀과 같은 종류), 값은 예열 뒤 2회입니다. 이 값은 저장 시간이고, 위 TTFT 와는 다른 양입니다.2번: 장치 플러그인이 설치된 호스트에서
MARU_PLUGINS를 비운 채 단위 시험 전체를 strace 아래에서 돌렸습니다. 플러그인 모듈 파일은 한 번도 열리지 않았습니다.3번, vLLM 서빙: 적재하는 엔진을 GPU 블록 64개(블록당 16 토큰), 청크 128 토큰,
maru_async_load, CXL-DRAM 장치로 띄웠습니다. 저장하는 엔진이 800 토큰(50블록)·400 토큰(25블록) 프롬프트의 앞 384 토큰씩을 Maru 에 넣은 뒤, 두 프롬프트를 한 completion 요청으로 함께 보냈습니다. main 은 3회 모두 45 s 시간 초과로 멈췄고, 그동안 실행 0·대기 2 였습니다. 이 PR 은 3회에 각각 0.31·0.09·0.09 s 만에 두 요청을 끝냈습니다. 두 요청의 Maru 적중 토큰 합(vLLM 지표external_prefix_cache_hits의 증가분)은 3회 모두 768(= 384 + 384)이었습니다.🔗 Related Issues (optional)
🌿 Related PRs (optional)
🌿 Related Branches (optional)
📦 Release Note (for auto-generation / write in English)
NEW
CHANGED
FIXED
IMPORTANT NOTES