[KV-offload][FS] : Batch store/load_block in C - #49152
Conversation
orozery
left a comment
There was a problem hiding this comment.
Thanks @varun-sundar-rabindranath !
I see fs_io.cpp is growing significantly with this.
Can we somehow reduce/simplify?
This is what Claude suggested:
Remove the IOResult struct and safe_* wrappers (~70 lines)
These are an over-engineered error-reporting mechanism. The C functions could just return the index of the first failing operation (or -1 for success) and let Python reconstruct the exception from errno + the path at that index:
// Instead of IOResult + safe_open + safe_write + safe_close + safe_rename + safe_unlink...
static Py_ssize_t _store_block(const char* tmp_path, const char* dest_path,
const char* src, size_t size) {
if (access(dest_path, F_OK) == 0) return 0; // already exists
int fd = open(tmp_path, O_CREAT | O_EXCL | O_WRONLY | kODirectFlag, 0644);
if (fd < 0) return -1;
ssize_t w = write(fd, src, size);
close(fd);
if (w < 0 || (size_t)w != size) { unlink(tmp_path); return -1; }
if (rename(tmp_path, dest_path) != 0) { unlink(tmp_path); return -1; }
return 0;
}
Then in the Python entry point: return the failing index, and Python raises using the path from its list + errno.
|
Thanks for the review @orozery . Not having the safe* functions and IOResult, made the |
81a5d91 to
558c239
Compare
|
Hi @orozery I could shave off ~70 lines from fs_io.cpp. I believe the PR is ready for another round of review. PTAL ! Thanks 🙌 |
d564561 to
7745fe4
Compare
orozery
left a comment
There was a problem hiding this comment.
LGTM! Thanks @varun-sundar-rabindranath !
7745fe4 to
db9f6c7
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
db9f6c7 to
e840c99
Compare
|
Hi @varun-sundar-rabindranath, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
364ca43 to
e802789
Compare
|
Hi @varun-sundar-rabindranath, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
Signed-off-by: Varun Sundar Rabindranath <varun-sundar-rabindranath@h100-01.nemg-001.lab.rdu2.dc.redhat.com> Signed-off-by: <>
Signed-off-by: Varun Sundar Rabindranath <varun-sundar-rabindranath@h100-01.nemg-001.lab.rdu2.dc.redhat.com> Signed-off-by: <>
Signed-off-by: Varun Sundar Rabindranath <varun-sundar-rabindranath@h100-01.nemg-001.lab.rdu2.dc.redhat.com> Signed-off-by: <>
22936b0 to
49595dc
Compare
…s a miss A failed secondary-tier load livelocks the requesting request (vllm-project#49176): the async lookup cache records a positive verdict per key and nothing corrects it on load failure, so the scheduler re-issues the same doomed promotion every step until the request is aborted. - On a failed load the owning tier marks the cached lookup verdict as a miss (mark_miss), from get_finished_jobs on the scheduler thread. A cached miss is returned on subsequent lookups without re-probing, so the request recomputes on the GPU and never retries it, which structurally cannot loop. The tier already learns of its own failed loads, so no new SecondaryTierManager API is needed. A key is enqueued for probing exactly once, so drain_results asserts that invariant, keeping a late or duplicate result from resurrecting a corrected verdict. - Loads are batched. The C loader reports how many blocks were read before the first failure, so the tier fills JobResult.successful_keys (vllm-project#50321) and marks only the failed block onward as a miss. The manager keeps the blocks that did load in the primary tier, so they stay a hit; only the failed tail is recomputed, and the miss clears when the request finishes. - load_block removes the block file only on a provable short read, in both the C (_load_block) and Python paths. Stores are atomic, so a too-short existing file is genuine corruption; removing it makes future requests miss instead of repeating the failed load. Open failures and read errors leave the file untouched, and a harmless close after a full read no longer fails the load, so a transient host hiccup is not turned into permanent data loss or a spurious miss. This narrows the delete-on-any-error added in vllm-project#49152. Lookup keeps upstream's access-based existence check. Tests: per-tier livelock regressions, partial-batch keep (loaded blocks stay a hit, only the failed tail misses), delete-on-short-read and transient-leaves-file across the C and Python paths, and mark_miss unit tests including the enqueue-once invariant. 345 tiering tests pass (356 with the fs_io_C extension built). Fixes vllm-project#49176 Signed-off-by: Robbie J <RobbieJ@users.noreply.github.com>
### What this PR does / why we need it? refer to: vllm-project/vllm#40020 , vllm-project/vllm#49152 , vllm-project/vllm#46713 and vllm-project/vllm#49734 , add multi-tier KV cache offloading framework, reusing the c operator from the upstream vllm for batch loading, saving, and searching. ### How was this patch tested? test reuslt: |num-clients/max-active-conversations|base(without offload, TTFT/TPOT)|with dram(TTFT/TPOT)|with dram+ssd(TTFT/TPOT)| | :---:|:---:|:---:|:---:| |8/24|1151.21/87.28|863.02/67.62|605.81/49.87| |16/48|7251.86/108.90|5679.01/83.75|3667.07/59.05| model script: ``` export TP=1 export MODEL_PATH=/nas/disk1/Qwen3-14B export MODEL_NAME=Qwen3-14B export PORT=10113 #export CUDA_VISIBLE_DEVICES=3 export ASCEND_RT_VISIBLE_DEVICES=4 python3 -m vllm.entrypoints.openai.api_server --host 0.0.0.0 --port ${PORT} --dtype bfloat16 --model ${MODEL_PATH} --served-model-name ${MODEL_NAME} --tensor-parallel-size ${TP} --gpu-memory-utilization 0.6 --no-enable-prefix-caching --max-model-len 32768 --trust-remote-code --kv-transfer-config '{ "kv_connector": "OffloadingConnector", "kv_role": "kv_both", "kv_connector_extra_config": { "spec_name": "TieringOffloadingSpec", "cpu_bytes_to_use": 10737418240, "block_size": 128, "eviction_policy": "lru", "secondary_tiers": [ { "type": "fs", "root_dir": "/mnt/kv_cache4", "n_read_threads": 32, "n_write_threads": 16 } ] } }' ``` test script: ``` export MODEL_NAME=/nas/disk1/Qwen3-14B python /model/vllm/benchmarks/multi_turn/benchmark_serving_multi_turn.py --url http://127.0.0.1:10113 --model $MODEL_NAME --served-model-name Qwen3-14B --seed 1234 --input-file /model/vllm/benchmarks/multi_turn/generate_multi_turn2.json \ --num-clients 8 --max-active-conversations 24 ``` generate_multi_turn2.json ``` { "filetype": "generate_conversations", "num_conversations": 96, "text_files": ["pg1184.txt"], "print_stats": false, "prompt_input": { "num_turns": { "distribution": "uniform", "min": 12, "max": 18 }, "common_prefix_num_tokens": { "distribution": "constant", "value": 2000 }, "prefix_num_tokens": { "distribution": "lognormal", "average": 2000, "max": 10000 }, "num_tokens": { "distribution": "uniform", "min": 240, "max": 320 } }, "prompt_output": { "num_tokens": { "distribution": "uniform", "min": 80, "max": 120 } } } ``` - vLLM main: vllm-project/vllm@e6bfe03 --------- Signed-off-by: HF-001 <1670186653@qq.com>
…10575) ### What this PR does / why we need it? refer to: vllm-project/vllm#40020 , vllm-project/vllm#49152 , vllm-project/vllm#46713 and vllm-project/vllm#49734 , add multi-tier KV cache offloading framework, reusing the c operator from the upstream vllm for batch loading, saving, and searching. ### How was this patch tested? test reuslt: |num-clients/max-active-conversations|base(without offload, TTFT/TPOT)|with dram(TTFT/TPOT)|with dram+ssd(TTFT/TPOT)| | :---:|:---:|:---:|:---:| |8/24|1151.21/87.28|863.02/67.62|605.81/49.87| |16/48|7251.86/108.90|5679.01/83.75|3667.07/59.05| model script: ``` export TP=1 export MODEL_PATH=/nas/disk1/Qwen3-14B export MODEL_NAME=Qwen3-14B export PORT=10113 #export CUDA_VISIBLE_DEVICES=3 export ASCEND_RT_VISIBLE_DEVICES=4 python3 -m vllm.entrypoints.openai.api_server --host 0.0.0.0 --port ${PORT} --dtype bfloat16 --model ${MODEL_PATH} --served-model-name ${MODEL_NAME} --tensor-parallel-size ${TP} --gpu-memory-utilization 0.6 --no-enable-prefix-caching --max-model-len 32768 --trust-remote-code --kv-transfer-config '{ "kv_connector": "OffloadingConnector", "kv_role": "kv_both", "kv_connector_extra_config": { "spec_name": "TieringOffloadingSpec", "cpu_bytes_to_use": 10737418240, "block_size": 128, "eviction_policy": "lru", "secondary_tiers": [ { "type": "fs", "root_dir": "/mnt/kv_cache4", "n_read_threads": 32, "n_write_threads": 16 } ] } }' ``` test script: ``` export MODEL_NAME=/nas/disk1/Qwen3-14B python /model/vllm/benchmarks/multi_turn/benchmark_serving_multi_turn.py --url http://127.0.0.1:10113 --model $MODEL_NAME --served-model-name Qwen3-14B --seed 1234 --input-file /model/vllm/benchmarks/multi_turn/generate_multi_turn2.json \ --num-clients 8 --max-active-conversations 24 ``` generate_multi_turn2.json ``` { "filetype": "generate_conversations", "num_conversations": 96, "text_files": ["pg1184.txt"], "print_stats": false, "prompt_input": { "num_turns": { "distribution": "uniform", "min": 12, "max": 18 }, "common_prefix_num_tokens": { "distribution": "constant", "value": 2000 }, "prefix_num_tokens": { "distribution": "lognormal", "average": 2000, "max": 10000 }, "num_tokens": { "distribution": "uniform", "min": 240, "max": 320 } }, "prompt_output": { "num_tokens": { "distribution": "uniform", "min": 80, "max": 120 } } } ``` - vLLM main: vllm-project/vllm@e6bfe03 --------- Signed-off-by: HF-001 <1670186653@qq.com>
…10575) ### What this PR does / why we need it? refer to: vllm-project/vllm#40020 , vllm-project/vllm#49152 , vllm-project/vllm#46713 and vllm-project/vllm#49734 , add multi-tier KV cache offloading framework, reusing the c operator from the upstream vllm for batch loading, saving, and searching. ### How was this patch tested? test reuslt: |num-clients/max-active-conversations|base(without offload, TTFT/TPOT)|with dram(TTFT/TPOT)|with dram+ssd(TTFT/TPOT)| | :---:|:---:|:---:|:---:| |8/24|1151.21/87.28|863.02/67.62|605.81/49.87| |16/48|7251.86/108.90|5679.01/83.75|3667.07/59.05| model script: ``` export TP=1 export MODEL_PATH=/nas/disk1/Qwen3-14B export MODEL_NAME=Qwen3-14B export PORT=10113 #export CUDA_VISIBLE_DEVICES=3 export ASCEND_RT_VISIBLE_DEVICES=4 python3 -m vllm.entrypoints.openai.api_server --host 0.0.0.0 --port ${PORT} --dtype bfloat16 --model ${MODEL_PATH} --served-model-name ${MODEL_NAME} --tensor-parallel-size ${TP} --gpu-memory-utilization 0.6 --no-enable-prefix-caching --max-model-len 32768 --trust-remote-code --kv-transfer-config '{ "kv_connector": "OffloadingConnector", "kv_role": "kv_both", "kv_connector_extra_config": { "spec_name": "TieringOffloadingSpec", "cpu_bytes_to_use": 10737418240, "block_size": 128, "eviction_policy": "lru", "secondary_tiers": [ { "type": "fs", "root_dir": "/mnt/kv_cache4", "n_read_threads": 32, "n_write_threads": 16 } ] } }' ``` test script: ``` export MODEL_NAME=/nas/disk1/Qwen3-14B python /model/vllm/benchmarks/multi_turn/benchmark_serving_multi_turn.py --url http://127.0.0.1:10113 --model $MODEL_NAME --served-model-name Qwen3-14B --seed 1234 --input-file /model/vllm/benchmarks/multi_turn/generate_multi_turn2.json \ --num-clients 8 --max-active-conversations 24 ``` generate_multi_turn2.json ``` { "filetype": "generate_conversations", "num_conversations": 96, "text_files": ["pg1184.txt"], "print_stats": false, "prompt_input": { "num_turns": { "distribution": "uniform", "min": 12, "max": 18 }, "common_prefix_num_tokens": { "distribution": "constant", "value": 2000 }, "prefix_num_tokens": { "distribution": "lognormal", "average": 2000, "max": 10000 }, "num_tokens": { "distribution": "uniform", "min": 240, "max": 320 } }, "prompt_output": { "num_tokens": { "distribution": "uniform", "min": 80, "max": 120 } } } ``` - vLLM main: vllm-project/vllm@e6bfe03 --------- Signed-off-by: HF-001 <1670186653@qq.com>
…10575) ### What this PR does / why we need it? refer to: vllm-project/vllm#40020 , vllm-project/vllm#49152 , vllm-project/vllm#46713 and vllm-project/vllm#49734 , add multi-tier KV cache offloading framework, reusing the c operator from the upstream vllm for batch loading, saving, and searching. ### How was this patch tested? test reuslt: |num-clients/max-active-conversations|base(without offload, TTFT/TPOT)|with dram(TTFT/TPOT)|with dram+ssd(TTFT/TPOT)| | :---:|:---:|:---:|:---:| |8/24|1151.21/87.28|863.02/67.62|605.81/49.87| |16/48|7251.86/108.90|5679.01/83.75|3667.07/59.05| model script: ``` export TP=1 export MODEL_PATH=/nas/disk1/Qwen3-14B export MODEL_NAME=Qwen3-14B export PORT=10113 #export CUDA_VISIBLE_DEVICES=3 export ASCEND_RT_VISIBLE_DEVICES=4 python3 -m vllm.entrypoints.openai.api_server --host 0.0.0.0 --port ${PORT} --dtype bfloat16 --model ${MODEL_PATH} --served-model-name ${MODEL_NAME} --tensor-parallel-size ${TP} --gpu-memory-utilization 0.6 --no-enable-prefix-caching --max-model-len 32768 --trust-remote-code --kv-transfer-config '{ "kv_connector": "OffloadingConnector", "kv_role": "kv_both", "kv_connector_extra_config": { "spec_name": "TieringOffloadingSpec", "cpu_bytes_to_use": 10737418240, "block_size": 128, "eviction_policy": "lru", "secondary_tiers": [ { "type": "fs", "root_dir": "/mnt/kv_cache4", "n_read_threads": 32, "n_write_threads": 16 } ] } }' ``` test script: ``` export MODEL_NAME=/nas/disk1/Qwen3-14B python /model/vllm/benchmarks/multi_turn/benchmark_serving_multi_turn.py --url http://127.0.0.1:10113 --model $MODEL_NAME --served-model-name Qwen3-14B --seed 1234 --input-file /model/vllm/benchmarks/multi_turn/generate_multi_turn2.json \ --num-clients 8 --max-active-conversations 24 ``` generate_multi_turn2.json ``` { "filetype": "generate_conversations", "num_conversations": 96, "text_files": ["pg1184.txt"], "print_stats": false, "prompt_input": { "num_turns": { "distribution": "uniform", "min": 12, "max": 18 }, "common_prefix_num_tokens": { "distribution": "constant", "value": 2000 }, "prefix_num_tokens": { "distribution": "lognormal", "average": 2000, "max": 10000 }, "num_tokens": { "distribution": "uniform", "min": 240, "max": 320 } }, "prompt_output": { "num_tokens": { "distribution": "uniform", "min": 80, "max": 120 } } } ``` - vLLM main: vllm-project/vllm@e6bfe03 --------- Signed-off-by: HF-001 <1670186653@qq.com> Signed-off-by: like-0517 <ithwlike@126.com>
Purpose
We have pools of python threads to read and write KV files from/to disk. These python threads compete for the GIL and submit bursty read/write commands to the disk. This prevents the disk from reaching 100% utilization.
Changes:
main: 1 request == 1 job == many keys == many reads/writes == each thread consumes a single read/write at a time.PR: 1 request == 1 job == many keys == many reads/writes == 1 thread does all the reads/writes.This 1 request to 1 thread mapping is likely detrimental at low concurrency. We can batch at the "keys" level and this I believe would be better to introduce in a followup PR and have this PR focus on the C implementation.
Performance
MODEL="openai/gpt-oss-120b"
TP_SIZE=2
CPU_BYTES=25769803776 # 25GB
guidellm bench command
The benchmark and vllm serve is setup to load KV from disk as much as possible.
main
n_read_threads=4, n_write_threads=4
n_read_threads=16, n_write_threads=16
PR
n_read_threads=4, n_write_threads=4
n_read_threads=16, n_write_threads=16
Things to note
main - 4 threadsthroughput : 3 GB/s ; utilization : mostly <60%main - 16 threadsthroughput : 3.8 GB/s; utilization : mostly <60%PR - 4 threadsthroughput : 4.5 GB/s; utilization : around 80%PR - 16 threadsthroughput : 4.5 GB/s; utilization : around 70%Note that the PR doesn't rely that much on the number of threads.
Test Plan
Compare lm-evals without and with offloading.
without offloading:with offloading:lm-eval command:Test Result
baseline: without offloadingwith offloading : run 1 - cold cachewith offloading: run 2 - warm cache