perf(kv-cache): cache get_avail_physical_pages in available_size to skip per-alloc cudaMemGetInfo - #456
Conversation
|
Thanks for your contribution! I think this change introduces a correctness issue. physical_free_pages = (
self._get_cached_avail_physical_pages()
+ self.page_allocator.get_num_reserved_pages())But the cached pages do not change after alloc that cause cached pages number is dirty. However, the cache is not updated or invalidated after alloc() maps new physical pages. As a result, subsequent calls to available_size() within the TTL may use a stale physical-page count and overestimate the available KV-cache capacity. This can be reproduced by changing def test_basic_alloc_free(setup_kvcache):
# instantiate a kv cache manager with known size
manager = setup_kvcache
# initial available blocks
initial_available = manager.available_size()
# allocate some blocks
# change this
n_blocks = 1024
handle = manager.alloc(n_blocks)
after_alloc = manager.available_size()
assert after_alloc + n_blocks == initial_available
# free the allocated blocks
manager.free(handle)
after_free = manager.available_size()
assert after_free == initial_availableResult |
|
Thanks for catching this — you are right. The cache served stale physical-free data after
The per-scheduler-step hot path ( Since |
available_size() runs per allocation (patches.py:792) and per scheduler step (:927); each call fires a cudaMemGetInfo driver call through page_allocator.get_avail_physical_pages() (csrc/page_allocator.cpp:481). Cache the result for a 100 ms window -- matching the resize_watcher poll interval (csrc/page_allocator.cpp:838) -- so one driver read serves the whole window instead of one per call. Invalidate on resize() and when in_shrink toggles so a resize/shrink is never served stale physical-free data. get_num_free_pages() and get_num_reserved_pages() stay uncached (cheap).
Restructure _get_cached_avail_physical_pages() to return the cached value on the fresh-hit branch so mypy narrows Optional[int] -> int, and drop the non-self type annotation in the test stub. No behavior change.
mypy 1.11.1 kept the local var's inferred Optional[int] type from the cache-field read and flagged the final return. Use a fresh int-annotated local for the fetched value so the Optional never reaches the return.
available_size() TTL-caches get_avail_physical_pages() to skip the per-call cudaMemGetInfo driver read. The cache was already invalidated on resize() and the in_shrink toggle, but not when alloc() maps a new physical page (alloc_page()) or when free()/clear() return pages to the driver, so available_size() could serve a stale physical-free count and overestimate KV-cache capacity within the TTL window. Drop the cached value at each physical-pool mutation (alloc_page, free_pages, clear) so the next available_size() re-reads the driver. Adds a CPU-only regression test mirroring the reported scenario: alloc maps a page and available_size() must drop by one page's worth of blocks instead of replaying the pre-alloc cached value.
2395135 to
faa9c7d
Compare
|
Rebased onto latest main to resolve a merge conflict in |
Summary
KVCacheManager.available_size()callsPageAllocator.get_avail_physical_pages()on every invocation, and that C++ method issues acudaMemGetInfodriver call.available_size()runs once per allocation request (patches.py:792) and once per scheduler step (:927), so the driver call fires on every alloc/step. This PR caches theget_avail_physical_pages()result for 100 ms — theresize_watcherpoll interval — and invalidates it onresize()and on thein_shrinktoggle, so the driver call fires at most once per 100 ms window instead of once per call.Motivation
benchmarks/bench_alloc/README.mddocuments a 12.5× allocation-throughput win attributed to droppingcudaMemGetInfofromavailable_size(). At HEAD (ac9680a) that drop did not happen: the call was relocated fromavailable_size()intoget_avail_physical_pages()(csrc/page_allocator.cpp:481), whichavailable_size()still calls (kvcached/kv_cache_manager.py:502). This PR delivers the documented win by caching the call rather than relocating it. Refs #299 (the "Reduce CUDA Call Overhead inavailable_size" track item); this PR takes the narrower TTL-cache approach rather than the issue's proposed C++ rewrite.Changes
kvcached/kv_cache_manager.py: add a TTL cache (value +time.monotonic()timestamp, 100 ms) forget_avail_physical_pages()inavailable_size(). Invalidate onresize(), on thein_shrinktoggle infree(), and onclear().get_num_free_pages()andget_num_reserved_pages()stay uncached (cheap / atomic).tests/test_available_size_cache.py(new, CPU-only): asserts the call count drops from N-per-available_size()to 1-per-100 ms window, refetches after the TTL expires, and refetches after a resize.Reviewer note — staleness bound
The physical free-page count can go stale for up to 100 ms between refreshes on a multitenant GPU where another process frees/allocates in that window. The staleness is bounded by the same 100 ms interval the allocator already tolerates for virtual free-page polling (the
resize_watchercadence), and an explicitresize()orin_shrinktransition invalidates immediately, so no stale value is served across a resize.Tests
python -m pytest tests/test_available_size_cache.py— 3 passed. The cache test is red onmain(no cache → call count N) and green on this branch. Existing CPU tests touchingavailable_size(test_prefix_cache,test_bestfit_page_selection,test_page_aware_eviction,test_alloc_rollback,test_observability) still pass. The new test is registered intests/manifests/cpu.txt;python tools/check_test_classification.pypasses.