lmcache-cli 0.4.5.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmcache/__init__.py +84 -0
- lmcache/_version.py +24 -0
- lmcache/cli/__init__.py +1 -0
- lmcache/cli/commands/__init__.py +34 -0
- lmcache/cli/commands/base.py +157 -0
- lmcache/cli/commands/bench/__init__.py +557 -0
- lmcache/cli/commands/bench/engine_bench/__init__.py +1 -0
- lmcache/cli/commands/bench/engine_bench/config.py +245 -0
- lmcache/cli/commands/bench/engine_bench/interactive/__init__.py +274 -0
- lmcache/cli/commands/bench/engine_bench/interactive/config.json +10 -0
- lmcache/cli/commands/bench/engine_bench/interactive/schema.py +352 -0
- lmcache/cli/commands/bench/engine_bench/interactive/state.py +327 -0
- lmcache/cli/commands/bench/engine_bench/interactive/terminal.py +291 -0
- lmcache/cli/commands/bench/engine_bench/progress.py +145 -0
- lmcache/cli/commands/bench/engine_bench/request_sender.py +232 -0
- lmcache/cli/commands/bench/engine_bench/stats.py +275 -0
- lmcache/cli/commands/bench/engine_bench/workloads/__init__.py +153 -0
- lmcache/cli/commands/bench/engine_bench/workloads/base.py +122 -0
- lmcache/cli/commands/bench/engine_bench/workloads/long_doc_permutator.py +435 -0
- lmcache/cli/commands/bench/engine_bench/workloads/long_doc_qa.py +281 -0
- lmcache/cli/commands/bench/engine_bench/workloads/multi_round_chat.py +337 -0
- lmcache/cli/commands/bench/engine_bench/workloads/random_prefill.py +178 -0
- lmcache/cli/commands/describe.py +310 -0
- lmcache/cli/commands/kvcache.py +133 -0
- lmcache/cli/commands/mock.py +75 -0
- lmcache/cli/commands/ping.py +113 -0
- lmcache/cli/commands/query/__init__.py +155 -0
- lmcache/cli/commands/query/prompt.py +134 -0
- lmcache/cli/commands/query/request.py +357 -0
- lmcache/cli/commands/server.py +99 -0
- lmcache/cli/commands/tool/__init__.py +63 -0
- lmcache/cli/commands/tool/cache_simulator.py +113 -0
- lmcache/cli/commands/trace/__init__.py +505 -0
- lmcache/cli/commands/trace/dispatch.py +249 -0
- lmcache/cli/commands/trace/driver.py +372 -0
- lmcache/cli/commands/trace/stats.py +289 -0
- lmcache/cli/documents/lmcache.txt +11 -0
- lmcache/cli/main.py +42 -0
- lmcache/cli/metrics/__init__.py +29 -0
- lmcache/cli/metrics/formatter.py +171 -0
- lmcache/cli/metrics/handler.py +94 -0
- lmcache/cli/metrics/metrics.py +161 -0
- lmcache/cli/metrics/section.py +77 -0
- lmcache/connections.py +173 -0
- lmcache/integration/__init__.py +2 -0
- lmcache/integration/base_service_factory.py +165 -0
- lmcache/integration/request_telemetry/__init__.py +1 -0
- lmcache/integration/request_telemetry/base.py +51 -0
- lmcache/integration/request_telemetry/factory.py +113 -0
- lmcache/integration/request_telemetry/fastapi.py +109 -0
- lmcache/integration/request_telemetry/noop.py +35 -0
- lmcache/integration/sglang/__init__.py +2 -0
- lmcache/integration/sglang/sglang_adapter.py +326 -0
- lmcache/integration/sglang/utils.py +39 -0
- lmcache/integration/vllm/__init__.py +1 -0
- lmcache/integration/vllm/lmcache_connector_v1.py +213 -0
- lmcache/integration/vllm/lmcache_connector_v1_085.py +150 -0
- lmcache/integration/vllm/lmcache_mp_connector_0180.py +1072 -0
- lmcache/integration/vllm/tests/test_mm_hash_utils.py +112 -0
- lmcache/integration/vllm/utils.py +433 -0
- lmcache/integration/vllm/vllm_multi_process_adapter.py +1090 -0
- lmcache/integration/vllm/vllm_service_factory.py +339 -0
- lmcache/integration/vllm/vllm_v1_adapter.py +1713 -0
- lmcache/logging.py +107 -0
- lmcache/native_storage_ops.pyi +230 -0
- lmcache/non_cuda_equivalents.py +1424 -0
- lmcache/observability.py +1958 -0
- lmcache/storage_backend/serde/__init__.py +1 -0
- lmcache/storage_backend/serde/cachegen_basics.py +210 -0
- lmcache/storage_backend/serde/cachegen_decoder.py +207 -0
- lmcache/storage_backend/serde/cachegen_encoder.py +394 -0
- lmcache/storage_backend/serde/serde.py +75 -0
- lmcache/tools/__init__.py +1 -0
- lmcache/tools/cache_simulator/README.md +392 -0
- lmcache/tools/cache_simulator/__init__.py +1 -0
- lmcache/tools/cache_simulator/docs/simulate_example.png +0 -0
- lmcache/tools/cache_simulator/docs/sweep_example.png +0 -0
- lmcache/tools/cache_simulator/gen_bench_dataset.py +360 -0
- lmcache/tools/cache_simulator/lru_cache.py +124 -0
- lmcache/tools/cache_simulator/plot_hit_rate.py +231 -0
- lmcache/tools/cache_simulator/simulator.py +795 -0
- lmcache/tools/controller_benchmark/README.md +161 -0
- lmcache/tools/controller_benchmark/__init__.py +1 -0
- lmcache/tools/controller_benchmark/__main__.py +331 -0
- lmcache/tools/controller_benchmark/benchmark.py +660 -0
- lmcache/tools/controller_benchmark/config.py +44 -0
- lmcache/tools/controller_benchmark/constants.py +10 -0
- lmcache/tools/controller_benchmark/handlers/__init__.py +46 -0
- lmcache/tools/controller_benchmark/handlers/admit.py +52 -0
- lmcache/tools/controller_benchmark/handlers/base.py +47 -0
- lmcache/tools/controller_benchmark/handlers/deregister.py +49 -0
- lmcache/tools/controller_benchmark/handlers/evict.py +52 -0
- lmcache/tools/controller_benchmark/handlers/heartbeat.py +56 -0
- lmcache/tools/controller_benchmark/handlers/p2p_lookup.py +47 -0
- lmcache/tools/controller_benchmark/handlers/register.py +56 -0
- lmcache/tools/mp_status_viewer/__init__.py +1 -0
- lmcache/tools/mp_status_viewer/__main__.py +95 -0
- lmcache/usage_context.py +417 -0
- lmcache/utils.py +665 -0
- lmcache/v1/__init__.py +2 -0
- lmcache/v1/api_server/__init__.py +2 -0
- lmcache/v1/api_server/__main__.py +537 -0
- lmcache/v1/basic_check.py +112 -0
- lmcache/v1/cache_controller/__init__.py +9 -0
- lmcache/v1/cache_controller/commands/__init__.py +15 -0
- lmcache/v1/cache_controller/commands/base.py +35 -0
- lmcache/v1/cache_controller/commands/full_sync.py +49 -0
- lmcache/v1/cache_controller/config.py +176 -0
- lmcache/v1/cache_controller/controller_manager.py +535 -0
- lmcache/v1/cache_controller/controllers/__init__.py +11 -0
- lmcache/v1/cache_controller/controllers/full_sync_tracker.py +473 -0
- lmcache/v1/cache_controller/controllers/kv_controller.py +439 -0
- lmcache/v1/cache_controller/controllers/registration_controller.py +282 -0
- lmcache/v1/cache_controller/executor.py +463 -0
- lmcache/v1/cache_controller/frontend/static/css/style.css +201 -0
- lmcache/v1/cache_controller/frontend/static/img/logo.png +0 -0
- lmcache/v1/cache_controller/frontend/static/index.html +234 -0
- lmcache/v1/cache_controller/frontend/static/js/controller_app.js +660 -0
- lmcache/v1/cache_controller/full_sync_sender.py +475 -0
- lmcache/v1/cache_controller/locks.py +149 -0
- lmcache/v1/cache_controller/message.py +828 -0
- lmcache/v1/cache_controller/observability.py +208 -0
- lmcache/v1/cache_controller/utils.py +679 -0
- lmcache/v1/cache_controller/worker.py +665 -0
- lmcache/v1/cache_engine.py +2058 -0
- lmcache/v1/cache_interface.py +19 -0
- lmcache/v1/check/__init__.py +74 -0
- lmcache/v1/check/check_mode_gen.py +86 -0
- lmcache/v1/check/check_mode_test_l2_adapter.py +284 -0
- lmcache/v1/check/check_mode_test_remote.py +155 -0
- lmcache/v1/check/check_mode_test_storage_manager.py +142 -0
- lmcache/v1/check/utils.py +571 -0
- lmcache/v1/compute/__init__.py +2 -0
- lmcache/v1/compute/attention/__init__.py +0 -0
- lmcache/v1/compute/attention/abstract.py +39 -0
- lmcache/v1/compute/attention/flash_attn.py +129 -0
- lmcache/v1/compute/attention/flash_infer_sparse.py +284 -0
- lmcache/v1/compute/attention/metadata.py +85 -0
- lmcache/v1/compute/attention/utils.py +14 -0
- lmcache/v1/compute/blend/__init__.py +7 -0
- lmcache/v1/compute/blend/blender.py +168 -0
- lmcache/v1/compute/blend/metadata.py +34 -0
- lmcache/v1/compute/blend/utils.py +63 -0
- lmcache/v1/compute/models/__init__.py +0 -0
- lmcache/v1/compute/models/base.py +141 -0
- lmcache/v1/compute/models/llama.py +9 -0
- lmcache/v1/compute/models/qwen3.py +24 -0
- lmcache/v1/compute/models/utils.py +68 -0
- lmcache/v1/compute/positional_encoding.py +199 -0
- lmcache/v1/config.py +848 -0
- lmcache/v1/config_base.py +848 -0
- lmcache/v1/distributed/api.py +248 -0
- lmcache/v1/distributed/config.py +321 -0
- lmcache/v1/distributed/error.py +64 -0
- lmcache/v1/distributed/eviction.py +192 -0
- lmcache/v1/distributed/eviction_policy/__init__.py +21 -0
- lmcache/v1/distributed/eviction_policy/factory.py +27 -0
- lmcache/v1/distributed/eviction_policy/lru.py +244 -0
- lmcache/v1/distributed/eviction_policy/noop.py +50 -0
- lmcache/v1/distributed/internal_api.py +170 -0
- lmcache/v1/distributed/l1_manager.py +835 -0
- lmcache/v1/distributed/l2_adapters/__init__.py +67 -0
- lmcache/v1/distributed/l2_adapters/base.py +360 -0
- lmcache/v1/distributed/l2_adapters/config.py +385 -0
- lmcache/v1/distributed/l2_adapters/factory.py +205 -0
- lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +747 -0
- lmcache/v1/distributed/l2_adapters/fs_native_l2_adapter.py +167 -0
- lmcache/v1/distributed/l2_adapters/mock_l2_adapter.py +516 -0
- lmcache/v1/distributed/l2_adapters/mooncake_store_l2_adapter.py +135 -0
- lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py +468 -0
- lmcache/v1/distributed/l2_adapters/native_plugin_l2_adapter.py +199 -0
- lmcache/v1/distributed/l2_adapters/nixl_store_dynamic_l2_adapter.py +831 -0
- lmcache/v1/distributed/l2_adapters/nixl_store_l2_adapter.py +983 -0
- lmcache/v1/distributed/l2_adapters/plugin_l2_adapter.py +210 -0
- lmcache/v1/distributed/l2_adapters/resp_l2_adapter.py +176 -0
- lmcache/v1/distributed/memory_manager.py +179 -0
- lmcache/v1/distributed/storage_controller.py +39 -0
- lmcache/v1/distributed/storage_controllers/__init__.py +43 -0
- lmcache/v1/distributed/storage_controllers/eviction_controller.py +242 -0
- lmcache/v1/distributed/storage_controllers/prefetch_controller.py +830 -0
- lmcache/v1/distributed/storage_controllers/prefetch_policy.py +193 -0
- lmcache/v1/distributed/storage_controllers/store_controller.py +452 -0
- lmcache/v1/distributed/storage_controllers/store_policy.py +213 -0
- lmcache/v1/distributed/storage_manager.py +532 -0
- lmcache/v1/event_manager.py +145 -0
- lmcache/v1/exceptions/__init__.py +16 -0
- lmcache/v1/gpu_connector/__init__.py +126 -0
- lmcache/v1/gpu_connector/gpu_connectors.py +1906 -0
- lmcache/v1/gpu_connector/gpu_ops.py +85 -0
- lmcache/v1/gpu_connector/hpu_connector.py +326 -0
- lmcache/v1/gpu_connector/mock_gpu_connector.py +67 -0
- lmcache/v1/gpu_connector/utils.py +890 -0
- lmcache/v1/gpu_connector/xpu_connectors.py +916 -0
- lmcache/v1/health_monitor/__init__.py +1 -0
- lmcache/v1/health_monitor/base.py +587 -0
- lmcache/v1/health_monitor/checks/__init__.py +1 -0
- lmcache/v1/health_monitor/checks/remote_backend_check.py +304 -0
- lmcache/v1/health_monitor/constants.py +36 -0
- lmcache/v1/internal_api_server/__init__.py +0 -0
- lmcache/v1/internal_api_server/api_registry.py +59 -0
- lmcache/v1/internal_api_server/api_server.py +120 -0
- lmcache/v1/internal_api_server/common/__init__.py +1 -0
- lmcache/v1/internal_api_server/common/env_api.py +22 -0
- lmcache/v1/internal_api_server/common/loglevel_api.py +57 -0
- lmcache/v1/internal_api_server/common/metrics_api.py +29 -0
- lmcache/v1/internal_api_server/common/periodic_thread_api.py +138 -0
- lmcache/v1/internal_api_server/common/run_script_api.py +73 -0
- lmcache/v1/internal_api_server/common/thread_api.py +63 -0
- lmcache/v1/internal_api_server/controller/__init__.py +1 -0
- lmcache/v1/internal_api_server/controller/key_stats_api.py +81 -0
- lmcache/v1/internal_api_server/controller/worker_info_api.py +136 -0
- lmcache/v1/internal_api_server/utils.py +43 -0
- lmcache/v1/internal_api_server/vllm/__init__.py +1 -0
- lmcache/v1/internal_api_server/vllm/backend_api.py +221 -0
- lmcache/v1/internal_api_server/vllm/bypass_api.py +204 -0
- lmcache/v1/internal_api_server/vllm/cache_api.py +895 -0
- lmcache/v1/internal_api_server/vllm/chunk_statistics_api.py +141 -0
- lmcache/v1/internal_api_server/vllm/conf_api.py +147 -0
- lmcache/v1/internal_api_server/vllm/freeze_api.py +172 -0
- lmcache/v1/internal_api_server/vllm/hot_cache_api.py +184 -0
- lmcache/v1/internal_api_server/vllm/inference_api.py +65 -0
- lmcache/v1/internal_api_server/vllm/load_fs_chunks_api.py +320 -0
- lmcache/v1/internal_api_server/vllm/lookup_api.py +145 -0
- lmcache/v1/internal_api_server/vllm/version_api.py +25 -0
- lmcache/v1/kv_layer_groups.py +267 -0
- lmcache/v1/lazy_memory_allocator.py +284 -0
- lmcache/v1/lookup_client/__init__.py +25 -0
- lmcache/v1/lookup_client/abstract_client.py +77 -0
- lmcache/v1/lookup_client/async_lookup_message.py +50 -0
- lmcache/v1/lookup_client/chunk_statistics_lookup_client.py +200 -0
- lmcache/v1/lookup_client/factory.py +251 -0
- lmcache/v1/lookup_client/hit_limit_lookup_client.py +86 -0
- lmcache/v1/lookup_client/lmcache_async_lookup_client.py +407 -0
- lmcache/v1/lookup_client/lmcache_lookup_client.py +285 -0
- lmcache/v1/lookup_client/lmcache_lookup_client_bypass.py +99 -0
- lmcache/v1/lookup_client/mooncake_lookup_client.py +87 -0
- lmcache/v1/lookup_client/record_strategies/__init__.py +77 -0
- lmcache/v1/lookup_client/record_strategies/base.py +327 -0
- lmcache/v1/lookup_client/record_strategies/file_hash.py +130 -0
- lmcache/v1/lookup_client/record_strategies/memory_bloom_filter.py +81 -0
- lmcache/v1/manager.py +539 -0
- lmcache/v1/memory_management.py +2619 -0
- lmcache/v1/metadata.py +114 -0
- lmcache/v1/mp_observability/AGENTS.override.md +21 -0
- lmcache/v1/mp_observability/README.md +204 -0
- lmcache/v1/mp_observability/config.py +340 -0
- lmcache/v1/mp_observability/event.py +100 -0
- lmcache/v1/mp_observability/event_bus.py +313 -0
- lmcache/v1/mp_observability/otel_init.py +145 -0
- lmcache/v1/mp_observability/subscribers/__init__.py +28 -0
- lmcache/v1/mp_observability/subscribers/logging/__init__.py +19 -0
- lmcache/v1/mp_observability/subscribers/logging/l1.py +56 -0
- lmcache/v1/mp_observability/subscribers/logging/l2.py +73 -0
- lmcache/v1/mp_observability/subscribers/logging/lookup_hash.py +209 -0
- lmcache/v1/mp_observability/subscribers/logging/mp_server.py +90 -0
- lmcache/v1/mp_observability/subscribers/logging/sm.py +59 -0
- lmcache/v1/mp_observability/subscribers/metrics/__init__.py +20 -0
- lmcache/v1/mp_observability/subscribers/metrics/l0_lifecycle.py +290 -0
- lmcache/v1/mp_observability/subscribers/metrics/l1.py +55 -0
- lmcache/v1/mp_observability/subscribers/metrics/l1_lifecycle.py +166 -0
- lmcache/v1/mp_observability/subscribers/metrics/l2.py +121 -0
- lmcache/v1/mp_observability/subscribers/metrics/sm.py +69 -0
- lmcache/v1/mp_observability/subscribers/tracing/__init__.py +12 -0
- lmcache/v1/mp_observability/subscribers/tracing/mp_server.py +333 -0
- lmcache/v1/mp_observability/subscribers/tracing/span_registry.py +148 -0
- lmcache/v1/mp_observability/trace/__init__.py +50 -0
- lmcache/v1/mp_observability/trace/codecs.py +255 -0
- lmcache/v1/mp_observability/trace/decorator.py +147 -0
- lmcache/v1/mp_observability/trace/format.py +132 -0
- lmcache/v1/mp_observability/trace/lifecycle.py +83 -0
- lmcache/v1/mp_observability/trace/reader.py +167 -0
- lmcache/v1/mp_observability/trace/recorder.py +300 -0
- lmcache/v1/multiprocess/__init__.py +0 -0
- lmcache/v1/multiprocess/affinity_pool.py +102 -0
- lmcache/v1/multiprocess/blend_server_v2.py +891 -0
- lmcache/v1/multiprocess/config.py +253 -0
- lmcache/v1/multiprocess/custom_types.py +281 -0
- lmcache/v1/multiprocess/futures.py +194 -0
- lmcache/v1/multiprocess/gpu_context.py +511 -0
- lmcache/v1/multiprocess/http_server.py +235 -0
- lmcache/v1/multiprocess/mp_runtime_plugin_launcher.py +130 -0
- lmcache/v1/multiprocess/mq.py +732 -0
- lmcache/v1/multiprocess/protocol.py +86 -0
- lmcache/v1/multiprocess/protocols/README.md +213 -0
- lmcache/v1/multiprocess/protocols/__init__.py +127 -0
- lmcache/v1/multiprocess/protocols/base.py +89 -0
- lmcache/v1/multiprocess/protocols/blend.py +109 -0
- lmcache/v1/multiprocess/protocols/blend_v2.py +57 -0
- lmcache/v1/multiprocess/protocols/controller.py +53 -0
- lmcache/v1/multiprocess/protocols/debug.py +34 -0
- lmcache/v1/multiprocess/protocols/engine.py +146 -0
- lmcache/v1/multiprocess/protocols/observability.py +39 -0
- lmcache/v1/multiprocess/server.py +1134 -0
- lmcache/v1/multiprocess/session.py +190 -0
- lmcache/v1/multiprocess/token_hasher.py +441 -0
- lmcache/v1/offload_server/__init__.py +17 -0
- lmcache/v1/offload_server/abstract_server.py +37 -0
- lmcache/v1/offload_server/message.py +30 -0
- lmcache/v1/offload_server/zmq_server.py +122 -0
- lmcache/v1/periodic_thread.py +579 -0
- lmcache/v1/pin_monitor.py +246 -0
- lmcache/v1/plugin/__init__.py +0 -0
- lmcache/v1/plugin/runtime_plugin_launcher.py +211 -0
- lmcache/v1/protocol.py +317 -0
- lmcache/v1/rpc/__init__.py +17 -0
- lmcache/v1/rpc/transport.py +105 -0
- lmcache/v1/rpc/zmq_transport.py +213 -0
- lmcache/v1/rpc_utils.py +165 -0
- lmcache/v1/server/__init__.py +2 -0
- lmcache/v1/server/__main__.py +170 -0
- lmcache/v1/server/storage_backend/__init__.py +21 -0
- lmcache/v1/server/storage_backend/abstract_backend.py +80 -0
- lmcache/v1/server/storage_backend/local_backend.py +75 -0
- lmcache/v1/server/utils.py +21 -0
- lmcache/v1/standalone/__init__.py +1 -0
- lmcache/v1/standalone/__main__.py +583 -0
- lmcache/v1/standalone/manager.py +80 -0
- lmcache/v1/standalone/standalone_service_factory.py +86 -0
- lmcache/v1/storage_backend/__init__.py +313 -0
- lmcache/v1/storage_backend/abstract_backend.py +445 -0
- lmcache/v1/storage_backend/audit_backend.py +233 -0
- lmcache/v1/storage_backend/batched_message_sender.py +222 -0
- lmcache/v1/storage_backend/cache_policy/__init__.py +45 -0
- lmcache/v1/storage_backend/cache_policy/base_policy.py +87 -0
- lmcache/v1/storage_backend/cache_policy/fifo.py +58 -0
- lmcache/v1/storage_backend/cache_policy/lfu.py +105 -0
- lmcache/v1/storage_backend/cache_policy/lru.py +81 -0
- lmcache/v1/storage_backend/cache_policy/mru.py +61 -0
- lmcache/v1/storage_backend/connector/__init__.py +443 -0
- lmcache/v1/storage_backend/connector/audit_adapter.py +77 -0
- lmcache/v1/storage_backend/connector/audit_connector.py +320 -0
- lmcache/v1/storage_backend/connector/base_connector.py +379 -0
- lmcache/v1/storage_backend/connector/blackhole_adapter.py +21 -0
- lmcache/v1/storage_backend/connector/blackhole_connector.py +37 -0
- lmcache/v1/storage_backend/connector/eic_adapter.py +31 -0
- lmcache/v1/storage_backend/connector/eic_connector.py +757 -0
- lmcache/v1/storage_backend/connector/external_adapter.py +79 -0
- lmcache/v1/storage_backend/connector/fs_adapter.py +51 -0
- lmcache/v1/storage_backend/connector/fs_connector.py +403 -0
- lmcache/v1/storage_backend/connector/infinistore_adapter.py +56 -0
- lmcache/v1/storage_backend/connector/infinistore_connector.py +177 -0
- lmcache/v1/storage_backend/connector/instrumented_connector.py +219 -0
- lmcache/v1/storage_backend/connector/lm_adapter.py +31 -0
- lmcache/v1/storage_backend/connector/lm_connector.py +176 -0
- lmcache/v1/storage_backend/connector/mock_adapter.py +57 -0
- lmcache/v1/storage_backend/connector/mock_connector.py +349 -0
- lmcache/v1/storage_backend/connector/mooncakestore_adapter.py +43 -0
- lmcache/v1/storage_backend/connector/mooncakestore_connector.py +614 -0
- lmcache/v1/storage_backend/connector/redis_adapter.py +181 -0
- lmcache/v1/storage_backend/connector/redis_connector.py +828 -0
- lmcache/v1/storage_backend/connector/s3_adapter.py +59 -0
- lmcache/v1/storage_backend/connector/s3_connector.py +699 -0
- lmcache/v1/storage_backend/connector/sagemaker_hyperpod_adapter.py +233 -0
- lmcache/v1/storage_backend/connector/sagemaker_hyperpod_connector.py +987 -0
- lmcache/v1/storage_backend/connector/valkey_adapter.py +114 -0
- lmcache/v1/storage_backend/connector/valkey_connector.py +627 -0
- lmcache/v1/storage_backend/gds_backend.py +1199 -0
- lmcache/v1/storage_backend/job_executor/__init__.py +0 -0
- lmcache/v1/storage_backend/job_executor/base_executor.py +34 -0
- lmcache/v1/storage_backend/job_executor/pq_executor.py +235 -0
- lmcache/v1/storage_backend/local_cpu_backend.py +810 -0
- lmcache/v1/storage_backend/local_disk_backend.py +656 -0
- lmcache/v1/storage_backend/maru_backend.py +734 -0
- lmcache/v1/storage_backend/naive_serde/__init__.py +50 -0
- lmcache/v1/storage_backend/naive_serde/cachegen_basics.py +133 -0
- lmcache/v1/storage_backend/naive_serde/cachegen_decoder.py +135 -0
- lmcache/v1/storage_backend/naive_serde/cachegen_encoder.py +83 -0
- lmcache/v1/storage_backend/naive_serde/kivi_serde.py +22 -0
- lmcache/v1/storage_backend/naive_serde/naive_serde.py +18 -0
- lmcache/v1/storage_backend/naive_serde/serde.py +37 -0
- lmcache/v1/storage_backend/native_clients/connector_client_base.py +165 -0
- lmcache/v1/storage_backend/native_clients/resp_client.py +35 -0
- lmcache/v1/storage_backend/nixl_storage_backend.py +1400 -0
- lmcache/v1/storage_backend/p2p_backend.py +788 -0
- lmcache/v1/storage_backend/path_sharder.py +117 -0
- lmcache/v1/storage_backend/pd_backend.py +646 -0
- lmcache/v1/storage_backend/plugins/dax_backend.py +1443 -0
- lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +1361 -0
- lmcache/v1/storage_backend/remote_backend.py +624 -0
- lmcache/v1/storage_backend/resp_client.py +227 -0
- lmcache/v1/storage_backend/storage_backend_listener.py +19 -0
- lmcache/v1/storage_backend/storage_manager.py +1352 -0
- lmcache/v1/system_detection.py +110 -0
- lmcache/v1/token_database.py +551 -0
- lmcache/v1/transfer_channel/__init__.py +83 -0
- lmcache/v1/transfer_channel/abstract.py +285 -0
- lmcache/v1/transfer_channel/mock_memory_channel.py +156 -0
- lmcache/v1/transfer_channel/nixl_channel.py +639 -0
- lmcache/v1/transfer_channel/py_socket_channel.py +260 -0
- lmcache/v1/transfer_channel/transfer_utils.py +63 -0
- lmcache/v1/utils/__init__.py +1 -0
- lmcache/v1/utils/bloom_filter.py +109 -0
- lmcache/v1/utils/cache_utils.py +125 -0
- lmcache_cli-0.4.5.dev0.dist-info/METADATA +185 -0
- lmcache_cli-0.4.5.dev0.dist-info/RECORD +399 -0
- lmcache_cli-0.4.5.dev0.dist-info/WHEEL +5 -0
- lmcache_cli-0.4.5.dev0.dist-info/entry_points.txt +2 -0
- lmcache_cli-0.4.5.dev0.dist-info/licenses/LICENSE +201 -0
- lmcache_cli-0.4.5.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2058 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Standard
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import (
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
Any,
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
Generator,
|
|
11
|
+
List,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
# First Party
|
|
19
|
+
from lmcache.v1.health_monitor.base import HealthMonitor
|
|
20
|
+
|
|
21
|
+
# Standard
|
|
22
|
+
import asyncio
|
|
23
|
+
import gc
|
|
24
|
+
import multiprocessing
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
# Third Party
|
|
28
|
+
import torch
|
|
29
|
+
|
|
30
|
+
# First Party
|
|
31
|
+
from lmcache.logging import init_logger
|
|
32
|
+
from lmcache.observability import LMCacheStatsLogger, LMCStatsMonitor
|
|
33
|
+
from lmcache.usage_context import InitializeUsageContext
|
|
34
|
+
from lmcache.utils import (
|
|
35
|
+
CacheEngineKey,
|
|
36
|
+
CacheStoreEvent,
|
|
37
|
+
_lmcache_nvtx_annotate,
|
|
38
|
+
compress_slot_mapping,
|
|
39
|
+
convert_tokens_to_list,
|
|
40
|
+
)
|
|
41
|
+
from lmcache.v1.config import LMCacheEngineConfig
|
|
42
|
+
from lmcache.v1.event_manager import EventManager, EventStatus, EventType
|
|
43
|
+
from lmcache.v1.gpu_connector.gpu_connectors import GPUConnectorInterface
|
|
44
|
+
from lmcache.v1.gpu_connector.utils import assert_layerwise_gpu_connector
|
|
45
|
+
from lmcache.v1.memory_management import CuFileMemoryAllocator # noqa: E501
|
|
46
|
+
from lmcache.v1.memory_management import ( # noqa: E501
|
|
47
|
+
MemoryAllocatorInterface,
|
|
48
|
+
MemoryFormat,
|
|
49
|
+
MemoryObj,
|
|
50
|
+
MemoryObjMetadata,
|
|
51
|
+
MixedMemoryAllocator,
|
|
52
|
+
PagedTensorMemoryAllocator,
|
|
53
|
+
TensorMemoryObj,
|
|
54
|
+
)
|
|
55
|
+
from lmcache.v1.metadata import LMCacheMetadata
|
|
56
|
+
from lmcache.v1.pin_monitor import PinMonitor
|
|
57
|
+
from lmcache.v1.storage_backend.storage_manager import StorageManager
|
|
58
|
+
from lmcache.v1.system_detection import NUMADetector, NUMAMapping
|
|
59
|
+
from lmcache.v1.token_database import (
|
|
60
|
+
ChunkedTokenDatabase,
|
|
61
|
+
SegmentTokenDatabase,
|
|
62
|
+
TokenDatabase,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger = init_logger(__name__)
|
|
66
|
+
|
|
67
|
+
# Type aliases for processed chunks
|
|
68
|
+
# (cache_key, memory_obj, start_index, end_index)
|
|
69
|
+
ProcessedChunk = Tuple[CacheEngineKey, MemoryObj, int, int]
|
|
70
|
+
# (list of processed chunks, total kv size)
|
|
71
|
+
ProcessTokensInternalResult = Tuple[List[ProcessedChunk], int]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CacheEngineEndSignal:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LMCacheEngine:
|
|
79
|
+
"""The main class for the cache engine.
|
|
80
|
+
|
|
81
|
+
When storing the KV caches into the cache engine, it takes GPU KV
|
|
82
|
+
caches from the serving engine and convert them into MemoryObjs that
|
|
83
|
+
resides in the CPU. The MemoryObjs are then being stored into the
|
|
84
|
+
StorageBackends in an asynchronous manner.
|
|
85
|
+
|
|
86
|
+
When retrieving the KV caches from the cache engine, it fetches the
|
|
87
|
+
MemoryObjs from the StorageBackends and convert them into GPU KV caches
|
|
88
|
+
by GPUConnectors specialized for the serving engine.
|
|
89
|
+
|
|
90
|
+
It also supports prefetching the KV caches from the StorageBackends.
|
|
91
|
+
It relies on the StorageBackends to manage the requests of prefetching
|
|
92
|
+
and real retrieval and avoid the conflicts.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
config: LMCacheEngineConfig,
|
|
98
|
+
metadata: LMCacheMetadata,
|
|
99
|
+
token_database: TokenDatabase,
|
|
100
|
+
gpu_connector: Optional[GPUConnectorInterface],
|
|
101
|
+
broadcast_fn: Callable[[torch.Tensor, int], None],
|
|
102
|
+
broadcast_object_fn: Callable[[Any, int], Any],
|
|
103
|
+
):
|
|
104
|
+
logger.info(f"Creating LMCacheEngine with config: {config}")
|
|
105
|
+
self.config = config
|
|
106
|
+
self.metadata = metadata
|
|
107
|
+
self.token_database = token_database
|
|
108
|
+
self.gpu_connector = gpu_connector
|
|
109
|
+
self.broadcast_fn = broadcast_fn
|
|
110
|
+
self.broadcast_object_fn = broadcast_object_fn
|
|
111
|
+
# save_only_first_rank only works when use mla
|
|
112
|
+
self.save_only_first_rank = (
|
|
113
|
+
self.config.get_extra_config_value("save_only_first_rank", metadata.use_mla)
|
|
114
|
+
and metadata.use_mla
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if self.save_only_first_rank and self.gpu_connector is not None:
|
|
118
|
+
self.broadcast_stream = (
|
|
119
|
+
self.gpu_connector.load_stream
|
|
120
|
+
if hasattr(self.gpu_connector, "load_stream")
|
|
121
|
+
else torch.cuda.Stream()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self.enable_controller = config.enable_controller
|
|
125
|
+
|
|
126
|
+
# NOTE: Unix systems use fork by default
|
|
127
|
+
multiprocessing.set_start_method("spawn", force=True)
|
|
128
|
+
|
|
129
|
+
# avoid circular import
|
|
130
|
+
# First Party
|
|
131
|
+
from lmcache.v1.cache_controller import LMCacheWorker
|
|
132
|
+
|
|
133
|
+
self.lmcache_worker: Optional[LMCacheWorker] = None
|
|
134
|
+
lmcache_worker_ids = config.get_lmcache_worker_ids(
|
|
135
|
+
metadata.use_mla, metadata.world_size
|
|
136
|
+
)
|
|
137
|
+
# lmcache_worker_ids is empty means start on all workers
|
|
138
|
+
if (
|
|
139
|
+
self.enable_controller
|
|
140
|
+
and self.metadata.role != "scheduler"
|
|
141
|
+
and (not lmcache_worker_ids or metadata.worker_id in lmcache_worker_ids)
|
|
142
|
+
):
|
|
143
|
+
self.lmcache_worker = LMCacheWorker(config, metadata, self)
|
|
144
|
+
else:
|
|
145
|
+
self.lmcache_worker = None
|
|
146
|
+
logger.info(
|
|
147
|
+
"LMCacheWorker is not initialized (related configs: "
|
|
148
|
+
"enable_controller: %s, role: %s, worker_id: %s, worker_ids: %s).",
|
|
149
|
+
self.enable_controller,
|
|
150
|
+
self.metadata.role,
|
|
151
|
+
self.metadata.worker_id,
|
|
152
|
+
lmcache_worker_ids,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self.async_loading = config.enable_async_loading
|
|
156
|
+
self.event_manager = EventManager()
|
|
157
|
+
|
|
158
|
+
self.use_layerwise = config.use_layerwise
|
|
159
|
+
|
|
160
|
+
# TODO: support save_only_first_rank when use layerwise
|
|
161
|
+
# if use_layerwise is True, all ranks will initialize the storage_manager
|
|
162
|
+
# if save_only_first_rank is False, all ranks will initialize
|
|
163
|
+
# the storage_manager
|
|
164
|
+
# if save_only_first_rank is True, only the first rank and
|
|
165
|
+
# lookup server workers will initialize the storage_manager
|
|
166
|
+
self.storage_manager: Optional[StorageManager] = None
|
|
167
|
+
|
|
168
|
+
# KV events
|
|
169
|
+
self.kv_events_enabled = False
|
|
170
|
+
self.kv_events_enabled = config.enable_kv_events
|
|
171
|
+
if self.kv_events_enabled:
|
|
172
|
+
self.kv_events: List[CacheStoreEvent] = []
|
|
173
|
+
logger.info("KV events are enabled.")
|
|
174
|
+
else:
|
|
175
|
+
logger.info("KV events are disabled.")
|
|
176
|
+
|
|
177
|
+
# HACK: remove this in the future
|
|
178
|
+
# NOTE (Jiayi): This is currently used to support
|
|
179
|
+
# dropping the kv cache from the buffer in PD backend
|
|
180
|
+
# at decoder.
|
|
181
|
+
self.remove_after_retrieve = config.enable_pd and config.pd_role == "receiver"
|
|
182
|
+
|
|
183
|
+
# asymmetric store/retrieve location can be specified
|
|
184
|
+
# this is typically used (but not limited) in PD system
|
|
185
|
+
self.store_location = config.store_location
|
|
186
|
+
self.retrieve_locations = config.retrieve_locations
|
|
187
|
+
|
|
188
|
+
self.num_layers = metadata.kv_shape[0]
|
|
189
|
+
self.fmt = None
|
|
190
|
+
if self.use_layerwise:
|
|
191
|
+
if metadata.use_mla:
|
|
192
|
+
self.fmt = MemoryFormat.KV_MLA_FMT
|
|
193
|
+
elif config.enable_blending:
|
|
194
|
+
self.fmt = MemoryFormat.KV_2TD
|
|
195
|
+
else:
|
|
196
|
+
self.fmt = MemoryFormat.KV_T2D
|
|
197
|
+
if metadata.use_mla:
|
|
198
|
+
self.fmt = MemoryFormat.KV_MLA_FMT
|
|
199
|
+
|
|
200
|
+
# NOTE(ApostaC): we haven't support lookup-cache yet
|
|
201
|
+
self.lookup_cache: dict[CacheEngineKey, Any] = {}
|
|
202
|
+
|
|
203
|
+
# lookup_id -> {location -> [pinned keys]}
|
|
204
|
+
self.lookup_pins: dict[str, dict[str, list]] = defaultdict(
|
|
205
|
+
lambda: defaultdict(list)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
InitializeUsageContext(config, metadata)
|
|
209
|
+
self.stats_monitor = LMCStatsMonitor.GetOrCreate()
|
|
210
|
+
# Initialize PinMonitor singleton with config
|
|
211
|
+
PinMonitor.GetOrCreate(config)
|
|
212
|
+
|
|
213
|
+
self.post_inited = False
|
|
214
|
+
|
|
215
|
+
# Flag to control KVCache Check logging (can be toggled via API)
|
|
216
|
+
self.kvcache_check_log_enabled = False
|
|
217
|
+
|
|
218
|
+
gc.collect()
|
|
219
|
+
if not config.py_enable_gc:
|
|
220
|
+
gc.disable()
|
|
221
|
+
|
|
222
|
+
# Health monitor reference (injected by LMCacheManager)
|
|
223
|
+
self._health_monitor: Optional["HealthMonitor"] = None
|
|
224
|
+
|
|
225
|
+
# Flag to indicate if initialization failed (irrecoverable error)
|
|
226
|
+
self._init_failed = False
|
|
227
|
+
|
|
228
|
+
def set_health_monitor(self, health_monitor: "HealthMonitor") -> None:
|
|
229
|
+
"""
|
|
230
|
+
Set the health monitor reference.
|
|
231
|
+
|
|
232
|
+
This is called by LMCacheManager after creating the HealthMonitor
|
|
233
|
+
to inject the reference into the engine.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
health_monitor: The HealthMonitor instance from LMCacheManager
|
|
237
|
+
"""
|
|
238
|
+
self._health_monitor = health_monitor
|
|
239
|
+
|
|
240
|
+
def is_healthy(self) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Check if the LMCache system is healthy.
|
|
243
|
+
|
|
244
|
+
This method returns False if:
|
|
245
|
+
- Initialization failed (irrecoverable error)
|
|
246
|
+
- HealthMonitor reports unhealthy
|
|
247
|
+
|
|
248
|
+
If no health monitor is set and initialization succeeded,
|
|
249
|
+
it returns True (assume healthy).
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
bool: True if healthy, False otherwise
|
|
253
|
+
"""
|
|
254
|
+
if self._init_failed:
|
|
255
|
+
return False
|
|
256
|
+
if self._health_monitor is not None:
|
|
257
|
+
return self._health_monitor.is_healthy()
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
def _get_req_id(self, kwargs: dict) -> str:
|
|
261
|
+
"""Extracts request ID from kwargs for logging."""
|
|
262
|
+
return kwargs.get("req_id", "unspecified")
|
|
263
|
+
|
|
264
|
+
def mark_init_failed(self, reason: str = "") -> None:
|
|
265
|
+
"""
|
|
266
|
+
Mark the engine as having failed initialization.
|
|
267
|
+
|
|
268
|
+
This is called by LMCacheManager when an irrecoverable error occurs
|
|
269
|
+
during initialization or post_init. Once marked, is_healthy() will
|
|
270
|
+
always return False, causing the system to fall back to recomputation.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
reason: Optional reason string for logging
|
|
274
|
+
"""
|
|
275
|
+
self._init_failed = True
|
|
276
|
+
if reason:
|
|
277
|
+
logger.error("LMCacheEngine marked as init failed: %s", reason)
|
|
278
|
+
else:
|
|
279
|
+
logger.error("LMCacheEngine marked as init failed")
|
|
280
|
+
|
|
281
|
+
def post_init(self, **kwargs) -> None:
|
|
282
|
+
if not self.post_inited:
|
|
283
|
+
logger.info("Post initializing LMCacheEngine")
|
|
284
|
+
lookup_server_worker_ids = self.config.get_lookup_server_worker_ids(
|
|
285
|
+
self.metadata.use_mla, self.metadata.world_size
|
|
286
|
+
)
|
|
287
|
+
if (
|
|
288
|
+
self.lmcache_worker is not None
|
|
289
|
+
or self.use_layerwise
|
|
290
|
+
or not self.save_only_first_rank
|
|
291
|
+
or self.metadata.is_first_rank()
|
|
292
|
+
or len(lookup_server_worker_ids) == 0
|
|
293
|
+
or self.metadata.worker_id in lookup_server_worker_ids
|
|
294
|
+
):
|
|
295
|
+
logger.info(
|
|
296
|
+
f"Initialize storage manager on rank {self.metadata.worker_id}, "
|
|
297
|
+
f"use layerwise: {self.use_layerwise},"
|
|
298
|
+
f"save only first rank: {self.save_only_first_rank}"
|
|
299
|
+
)
|
|
300
|
+
async_lookup_server = kwargs.get("async_lookup_server", None)
|
|
301
|
+
self.storage_manager = StorageManager(
|
|
302
|
+
self.config,
|
|
303
|
+
self.metadata,
|
|
304
|
+
event_manager=self.event_manager,
|
|
305
|
+
lmcache_worker=self.lmcache_worker,
|
|
306
|
+
async_lookup_server=async_lookup_server,
|
|
307
|
+
)
|
|
308
|
+
self.post_inited = True
|
|
309
|
+
|
|
310
|
+
def freeze(self, enabled: bool) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Set the freeze mode for the cache engine.
|
|
313
|
+
|
|
314
|
+
When freeze mode is enabled:
|
|
315
|
+
- All store operations will be skipped (no new data stored)
|
|
316
|
+
- Only local_cpu backend will be used for retrieval
|
|
317
|
+
- No admit/evict messages will be generated
|
|
318
|
+
This protects the local_cpu hot cache from changes.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
enabled (bool): Whether to enable freeze mode
|
|
322
|
+
"""
|
|
323
|
+
if self.storage_manager is not None:
|
|
324
|
+
self.storage_manager.set_freeze(enabled)
|
|
325
|
+
|
|
326
|
+
def is_frozen(self) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Get the current freeze mode status.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
bool: True if freeze mode is enabled, False otherwise
|
|
332
|
+
"""
|
|
333
|
+
if self.storage_manager is not None:
|
|
334
|
+
return self.storage_manager.is_frozen()
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
def set_hot_cache(self, enabled: bool) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Dynamically enable or disable the LocalCPUBackend hot cache.
|
|
340
|
+
|
|
341
|
+
When disabled, the existing hot cache entries will be cleared
|
|
342
|
+
and no new data will be written to the hot cache.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
enabled (bool): Whether to enable hot cache
|
|
346
|
+
"""
|
|
347
|
+
if self.storage_manager is not None:
|
|
348
|
+
self.storage_manager.set_hot_cache(enabled)
|
|
349
|
+
|
|
350
|
+
def is_hot_cache_enabled(self) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
Get the current hot cache status of LocalCPUBackend.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
bool: True if hot cache is enabled, False otherwise
|
|
356
|
+
"""
|
|
357
|
+
if self.storage_manager is not None:
|
|
358
|
+
return self.storage_manager.is_hot_cache_enabled()
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
@_lmcache_nvtx_annotate
|
|
362
|
+
@torch.inference_mode()
|
|
363
|
+
def store(
|
|
364
|
+
self,
|
|
365
|
+
tokens: Optional[Union[torch.Tensor, list[int]]] = None,
|
|
366
|
+
hashes: Optional[List[int]] = None,
|
|
367
|
+
offsets: Optional[List[int]] = None,
|
|
368
|
+
mask: Optional[torch.Tensor] = None,
|
|
369
|
+
**kwargs,
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Store the tokens/hashes and mask into the cache engine.
|
|
372
|
+
|
|
373
|
+
:param Optional[torch.Tensor] tokens: The tokens of the corresponding KV caches.
|
|
374
|
+
|
|
375
|
+
:param Optional[List[int]] hashes: The hashes of the corresponding KV caches.
|
|
376
|
+
|
|
377
|
+
:param Optional[torch.Tensor] mask: The mask for the tokens. Should
|
|
378
|
+
have the same length as tokens. And the mask should ALWAYS be like
|
|
379
|
+
FFFFFTTTTTTT, where True means the tokens needs to be matched,
|
|
380
|
+
and the Falses will ALWAYS be at the PREFIX of the tensor.
|
|
381
|
+
|
|
382
|
+
:param **kwargs: The additional arguments for the storage backend which
|
|
383
|
+
will be passed into the gpu_connector.
|
|
384
|
+
Should include KV cache specific information (e.g., paged KV buffer
|
|
385
|
+
and the page tables).
|
|
386
|
+
|
|
387
|
+
:raises: ValueError if the number of Falses in the mask is not a
|
|
388
|
+
multiple of the chunk size.
|
|
389
|
+
"""
|
|
390
|
+
# Health check: block operation if LMCache is unhealthy
|
|
391
|
+
if not self.is_healthy():
|
|
392
|
+
logger.warning("LMCache is unhealthy, skipping store operation")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
assert self.gpu_connector is not None, (
|
|
396
|
+
"gpu_connector is required for store operation"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if self._is_passive():
|
|
400
|
+
logger.debug(f"rank={self.metadata.worker_id} ignore store")
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
assert self.storage_manager is not None
|
|
404
|
+
|
|
405
|
+
# Get req_id for logging
|
|
406
|
+
req_id = self._get_req_id(kwargs)
|
|
407
|
+
|
|
408
|
+
# Initialize num_to_store_tokens to avoid reference before assignment
|
|
409
|
+
num_to_store_tokens = 0
|
|
410
|
+
|
|
411
|
+
if mask is not None:
|
|
412
|
+
num_to_store_tokens = torch.sum(mask).item()
|
|
413
|
+
elif tokens is not None:
|
|
414
|
+
num_to_store_tokens = len(tokens)
|
|
415
|
+
elif hashes is not None:
|
|
416
|
+
assert offsets is not None, (
|
|
417
|
+
"Offsets should be set when hashes are provided during store"
|
|
418
|
+
)
|
|
419
|
+
num_to_store_tokens = sum(offsets)
|
|
420
|
+
kwargs["slot_mapping"] = torch.tensor(
|
|
421
|
+
kwargs["slot_mapping"], dtype=torch.long, device="cuda"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
assert tokens is not None or hashes is not None, (
|
|
425
|
+
"Either 'tokens' or 'hashes' must be provided."
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# KVCache Check logging
|
|
429
|
+
self._log_kvcache_for_check(
|
|
430
|
+
operation="Store",
|
|
431
|
+
kwargs=kwargs,
|
|
432
|
+
token_count=num_to_store_tokens,
|
|
433
|
+
require_req_id=False,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Check if freeze mode is enabled
|
|
437
|
+
if self.is_frozen():
|
|
438
|
+
logger.debug(
|
|
439
|
+
"Freeze mode enabled, skipping store operation for %d tokens",
|
|
440
|
+
num_to_store_tokens,
|
|
441
|
+
)
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
store_stats = self.stats_monitor.on_store_request(num_to_store_tokens)
|
|
445
|
+
|
|
446
|
+
starts: List[int] = []
|
|
447
|
+
ends: List[int] = []
|
|
448
|
+
keys: List[CacheEngineKey] = []
|
|
449
|
+
memory_objs: List[MemoryObj] = []
|
|
450
|
+
|
|
451
|
+
tot_kv_size = 0
|
|
452
|
+
tot_token_num = 0
|
|
453
|
+
|
|
454
|
+
request_configs = kwargs.get("request_configs")
|
|
455
|
+
if request_configs is not None and len(request_configs) != 0:
|
|
456
|
+
assert isinstance(request_configs, dict)
|
|
457
|
+
|
|
458
|
+
with store_stats.profile_process_tokens():
|
|
459
|
+
prev_key = 0
|
|
460
|
+
for start, end, key in self.token_database.process_tokens(
|
|
461
|
+
tokens,
|
|
462
|
+
hashes,
|
|
463
|
+
offsets,
|
|
464
|
+
mask,
|
|
465
|
+
request_configs=request_configs,
|
|
466
|
+
):
|
|
467
|
+
assert isinstance(key, CacheEngineKey)
|
|
468
|
+
# Allocate the memory object
|
|
469
|
+
num_tokens = end - start
|
|
470
|
+
kv_shapes = self.metadata.get_shapes(num_tokens)
|
|
471
|
+
kv_dtypes = self.metadata.get_dtypes()
|
|
472
|
+
|
|
473
|
+
# TODO (Jiayi): should be batched in the future
|
|
474
|
+
memory_obj = self.storage_manager.allocate(
|
|
475
|
+
kv_shapes,
|
|
476
|
+
kv_dtypes,
|
|
477
|
+
busy_loop=self.config.get_extra_config_value(
|
|
478
|
+
"force_store_wait", False
|
|
479
|
+
),
|
|
480
|
+
fmt=self.fmt,
|
|
481
|
+
)
|
|
482
|
+
if memory_obj is None:
|
|
483
|
+
logger.warning(
|
|
484
|
+
"Local cpu memory under pressure so"
|
|
485
|
+
" choosing to store only "
|
|
486
|
+
f" {len(memory_objs)}"
|
|
487
|
+
" total chunks of KV cache."
|
|
488
|
+
)
|
|
489
|
+
break
|
|
490
|
+
|
|
491
|
+
starts.append(start)
|
|
492
|
+
ends.append(end)
|
|
493
|
+
keys.append(key)
|
|
494
|
+
memory_objs.append(memory_obj)
|
|
495
|
+
tot_kv_size += memory_obj.get_size()
|
|
496
|
+
tot_token_num += num_tokens
|
|
497
|
+
|
|
498
|
+
# Create KV event
|
|
499
|
+
if self.kv_events_enabled:
|
|
500
|
+
stored_event = CacheStoreEvent(
|
|
501
|
+
block_hashes=[key.chunk_hash],
|
|
502
|
+
parent_block_hash=None if start == 0 else prev_key,
|
|
503
|
+
token_ids=[],
|
|
504
|
+
block_size=num_tokens,
|
|
505
|
+
lora_id=None,
|
|
506
|
+
medium="cpu",
|
|
507
|
+
lora_name=None,
|
|
508
|
+
)
|
|
509
|
+
if tokens is not None:
|
|
510
|
+
stored_event.token_ids = convert_tokens_to_list(
|
|
511
|
+
tokens,
|
|
512
|
+
start,
|
|
513
|
+
end,
|
|
514
|
+
)
|
|
515
|
+
if isinstance(tokens, torch.Tensor):
|
|
516
|
+
stored_event.medium = tokens.device
|
|
517
|
+
elif hashes is not None:
|
|
518
|
+
stored_event.token_ids = hashes[start : end + 1]
|
|
519
|
+
logger.debug(
|
|
520
|
+
(
|
|
521
|
+
"Added kv cache event '%s' to kv cache events queue"
|
|
522
|
+
% stored_event
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
self.kv_events.append(stored_event)
|
|
526
|
+
prev_key = key.chunk_hash
|
|
527
|
+
|
|
528
|
+
# memory_objs might be empty, directly return to avoid sending tokens
|
|
529
|
+
if not memory_objs:
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
with store_stats.profile_from_gpu():
|
|
533
|
+
self.gpu_connector.batched_from_gpu(memory_objs, starts, ends, **kwargs)
|
|
534
|
+
|
|
535
|
+
with store_stats.profile_put():
|
|
536
|
+
transfer_spec = kwargs.get("transfer_spec", None)
|
|
537
|
+
# TODO: we implicitly rely on batched_put to call ref_count_down
|
|
538
|
+
# this management should be done in a cleaner way
|
|
539
|
+
self.storage_manager.batched_put(
|
|
540
|
+
keys,
|
|
541
|
+
memory_objs,
|
|
542
|
+
transfer_spec=transfer_spec,
|
|
543
|
+
location=self.store_location,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
self.stats_monitor.on_store_finished(
|
|
547
|
+
store_stats,
|
|
548
|
+
tot_token_num,
|
|
549
|
+
)
|
|
550
|
+
tot_time = store_stats.time_to_store()
|
|
551
|
+
|
|
552
|
+
logger.info(
|
|
553
|
+
"[req_id=%s] Stored %d out of total %d tokens. "
|
|
554
|
+
"size: %.4f GB, cost %.4f ms, throughput: %.4f GB/s; "
|
|
555
|
+
"offload_time: %.4f ms, put_time: %.4f ms",
|
|
556
|
+
req_id,
|
|
557
|
+
tot_token_num,
|
|
558
|
+
num_to_store_tokens,
|
|
559
|
+
tot_kv_size / 1024**3,
|
|
560
|
+
tot_time * 1000,
|
|
561
|
+
tot_kv_size / tot_time / 1024**3 if tot_time > 0 else 0,
|
|
562
|
+
(store_stats.process_tokens_time + store_stats.from_gpu_time) * 1000,
|
|
563
|
+
store_stats.put_time * 1000,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
@_lmcache_nvtx_annotate
|
|
567
|
+
@torch.inference_mode()
|
|
568
|
+
def store_layer(
|
|
569
|
+
self,
|
|
570
|
+
tokens: Union[torch.Tensor, list[int]],
|
|
571
|
+
mask: Optional[torch.Tensor] = None,
|
|
572
|
+
**kwargs,
|
|
573
|
+
) -> Generator[None, None, None]:
|
|
574
|
+
"""
|
|
575
|
+
Store the KV cache in a layerwise manner.
|
|
576
|
+
|
|
577
|
+
:param torch.Tensor tokens: The tokens of the corresponding KV caches.
|
|
578
|
+
|
|
579
|
+
:param Optional[torch.Tensor] mask: The mask for the tokens. Should
|
|
580
|
+
have the same length as tokens. And the mask should ALWAYS be like
|
|
581
|
+
FFFFFTTTTTTT, where True means the tokens needs to be matched.
|
|
582
|
+
|
|
583
|
+
:param **kwargs: The additional arguments for the storage backend which
|
|
584
|
+
will be passed into the gpu_connector.
|
|
585
|
+
|
|
586
|
+
return: A generator that yields None. In the first iteration, the
|
|
587
|
+
generator allocates the memory objects for all layers and moves
|
|
588
|
+
the KV cache of the first layer from GPU to CPU. In the next
|
|
589
|
+
iterations, it moves the KV cache of layer i from GPU to the memory
|
|
590
|
+
objects (on CPU) and puts the memory objects of layer i-1 to the
|
|
591
|
+
storage backends. In the last iteration, it puts the memory objects
|
|
592
|
+
of the last layer to the storage backends.
|
|
593
|
+
"""
|
|
594
|
+
# Health check: block operation if LMCache is unhealthy
|
|
595
|
+
if not self.is_healthy():
|
|
596
|
+
logger.warning("LMCache is unhealthy, skipping store_layer operation")
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
assert self.storage_manager is not None
|
|
600
|
+
assert self.gpu_connector is not None, (
|
|
601
|
+
"gpu_connector is required for store_layer operation"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Get req_id for logging
|
|
605
|
+
req_id = self._get_req_id(kwargs)
|
|
606
|
+
|
|
607
|
+
if mask is not None:
|
|
608
|
+
num_to_store_tokens = torch.sum(mask).item()
|
|
609
|
+
else:
|
|
610
|
+
num_to_store_tokens = len(tokens)
|
|
611
|
+
|
|
612
|
+
# KVCache Check logging
|
|
613
|
+
self._log_kvcache_for_check(
|
|
614
|
+
operation="Layerwise store",
|
|
615
|
+
kwargs=kwargs,
|
|
616
|
+
token_count=num_to_store_tokens,
|
|
617
|
+
require_req_id=True,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
monitor_req_id = self.stats_monitor.on_store_request(num_to_store_tokens)
|
|
621
|
+
|
|
622
|
+
# Check if freeze mode is enabled
|
|
623
|
+
if self.is_frozen():
|
|
624
|
+
logger.debug(
|
|
625
|
+
"Freeze mode enabled, skipping store_layer for %d tokens",
|
|
626
|
+
num_to_store_tokens,
|
|
627
|
+
)
|
|
628
|
+
# Still need to yield to avoid StopIteration
|
|
629
|
+
for layer_id in range(self.num_layers):
|
|
630
|
+
yield
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
starts = []
|
|
634
|
+
ends = []
|
|
635
|
+
keys = []
|
|
636
|
+
memory_objs = []
|
|
637
|
+
tot_token_num = 0
|
|
638
|
+
kv_dtype = self.metadata.kv_dtype
|
|
639
|
+
request_configs = kwargs.get("request_configs")
|
|
640
|
+
if request_configs is not None and len(request_configs) != 0:
|
|
641
|
+
assert isinstance(request_configs, dict)
|
|
642
|
+
|
|
643
|
+
prev_key = 0
|
|
644
|
+
for start, end, key in self.token_database.process_tokens(
|
|
645
|
+
tokens=tokens, mask=mask, request_configs=request_configs
|
|
646
|
+
):
|
|
647
|
+
assert isinstance(key, CacheEngineKey)
|
|
648
|
+
|
|
649
|
+
keys_multi_layer = key.split_layers(self.num_layers)
|
|
650
|
+
# Only check the first layer
|
|
651
|
+
if self.storage_manager.contains(
|
|
652
|
+
keys_multi_layer[0], self.retrieve_locations
|
|
653
|
+
):
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
# Allocate the memory object
|
|
657
|
+
num_tokens = end - start
|
|
658
|
+
kv_shape_single_layer = self.gpu_connector.get_shape(num_tokens)
|
|
659
|
+
|
|
660
|
+
memory_objs_multi_layer = self.storage_manager.batched_allocate(
|
|
661
|
+
kv_shape_single_layer,
|
|
662
|
+
kv_dtype,
|
|
663
|
+
batch_size=self.num_layers,
|
|
664
|
+
fmt=self.fmt,
|
|
665
|
+
busy_loop=self.config.get_extra_config_value("force_store_wait", False),
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if memory_objs_multi_layer is None:
|
|
669
|
+
logger.warning(
|
|
670
|
+
"Local cpu memory under pressure so"
|
|
671
|
+
" choosing to not store the KV cache."
|
|
672
|
+
)
|
|
673
|
+
break
|
|
674
|
+
|
|
675
|
+
starts.append(start)
|
|
676
|
+
ends.append(end)
|
|
677
|
+
keys.append(keys_multi_layer)
|
|
678
|
+
memory_objs.append(memory_objs_multi_layer)
|
|
679
|
+
tot_token_num += num_tokens
|
|
680
|
+
|
|
681
|
+
# Create KV event
|
|
682
|
+
if self.kv_events_enabled and tokens is not None:
|
|
683
|
+
stored_event = CacheStoreEvent(
|
|
684
|
+
block_hashes=[key.chunk_hash],
|
|
685
|
+
parent_block_hash=None if start == 0 else prev_key,
|
|
686
|
+
token_ids=[],
|
|
687
|
+
block_size=num_tokens,
|
|
688
|
+
lora_id=None,
|
|
689
|
+
medium="cpu",
|
|
690
|
+
lora_name=None,
|
|
691
|
+
)
|
|
692
|
+
if tokens is not None:
|
|
693
|
+
stored_event.token_ids = convert_tokens_to_list(
|
|
694
|
+
tokens,
|
|
695
|
+
start,
|
|
696
|
+
end,
|
|
697
|
+
)
|
|
698
|
+
if isinstance(tokens, torch.Tensor):
|
|
699
|
+
stored_event.medium = tokens.device
|
|
700
|
+
logger.debug(
|
|
701
|
+
f"Added kv cache event '{stored_event}' to kv cache events queue"
|
|
702
|
+
)
|
|
703
|
+
self.kv_events.append(stored_event)
|
|
704
|
+
prev_key = key.chunk_hash
|
|
705
|
+
|
|
706
|
+
if keys:
|
|
707
|
+
# Transpose the keys and memory objects into layer major format
|
|
708
|
+
memory_objs = [list(row) for row in zip(*memory_objs, strict=False)]
|
|
709
|
+
keys = [list(row) for row in zip(*keys, strict=False)]
|
|
710
|
+
|
|
711
|
+
# Calculate total KV size for logging
|
|
712
|
+
tot_kv_size = sum(
|
|
713
|
+
mo.get_size() for layer_objs in memory_objs for mo in layer_objs
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
assert_layerwise_gpu_connector(self.gpu_connector)
|
|
717
|
+
|
|
718
|
+
t_start = time.perf_counter()
|
|
719
|
+
mem_obj_generator = self.gpu_connector.batched_from_gpu(
|
|
720
|
+
memory_objs, starts, ends, **kwargs
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
next(mem_obj_generator)
|
|
724
|
+
|
|
725
|
+
for layer_id in range(self.num_layers):
|
|
726
|
+
yield
|
|
727
|
+
next(mem_obj_generator)
|
|
728
|
+
self.storage_manager.batched_put(
|
|
729
|
+
keys[layer_id], memory_objs[layer_id], location=self.store_location
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
tot_time = time.perf_counter() - t_start
|
|
733
|
+
logger.info(
|
|
734
|
+
"[req_id=%s] Stored %d out of total %d tokens. "
|
|
735
|
+
"size: %.4f GB, cost %.4f ms, throughput: %.4f GB/s",
|
|
736
|
+
req_id,
|
|
737
|
+
tot_token_num,
|
|
738
|
+
len(tokens),
|
|
739
|
+
tot_kv_size / 1024**3,
|
|
740
|
+
tot_time * 1000,
|
|
741
|
+
tot_kv_size / tot_time / 1024**3 if tot_time > 0 else 0,
|
|
742
|
+
)
|
|
743
|
+
else:
|
|
744
|
+
# If no cache are found, we still need to yield to avoid
|
|
745
|
+
# `StopIteration`
|
|
746
|
+
for layer_id in range(self.num_layers):
|
|
747
|
+
yield
|
|
748
|
+
|
|
749
|
+
self.stats_monitor.on_store_finished(monitor_req_id, tot_token_num)
|
|
750
|
+
yield
|
|
751
|
+
|
|
752
|
+
@_lmcache_nvtx_annotate
|
|
753
|
+
@torch.inference_mode()
|
|
754
|
+
def retrieve(
|
|
755
|
+
self,
|
|
756
|
+
tokens: Union[torch.Tensor, list[int]],
|
|
757
|
+
mask: Optional[torch.Tensor] = None,
|
|
758
|
+
**kwargs,
|
|
759
|
+
) -> torch.Tensor:
|
|
760
|
+
"""Retrieve the KV caches from the cache engine. And put the retrieved
|
|
761
|
+
KV cache to the serving engine via the GPU connector.
|
|
762
|
+
|
|
763
|
+
:param torch.Tensor tokens: The tokens of the corresponding KV caches.
|
|
764
|
+
|
|
765
|
+
:param Optional[torch.Tensor] mask: The mask for the tokens. Should
|
|
766
|
+
have the same length as tokens. And the mask should ALWAYS be like
|
|
767
|
+
FFFFFTTTTTTT, where True means the tokens needs to be matched,
|
|
768
|
+
and the Falses will ALWAYS be at the PREFIX of the tensor.
|
|
769
|
+
|
|
770
|
+
:param **kwargs: The additional arguments for the storage backend which
|
|
771
|
+
will be passed into the gpu_connector.
|
|
772
|
+
Should include KV cache specific information (e.g., paged KV buffer
|
|
773
|
+
and the page tables).
|
|
774
|
+
|
|
775
|
+
:return: the boolean mask indicating which tokens are retrieved. The
|
|
776
|
+
length of the mask should be the same as the tokens. On CPU.
|
|
777
|
+
|
|
778
|
+
:raises: ValueError if the number of Falses in the mask is not a
|
|
779
|
+
multiple of the chunk size.
|
|
780
|
+
"""
|
|
781
|
+
# Health check: block operation if LMCache is unhealthy
|
|
782
|
+
if not self.is_healthy():
|
|
783
|
+
logger.warning("LMCache is unhealthy, skipping retrieve operation")
|
|
784
|
+
return torch.zeros(len(tokens), dtype=torch.bool)
|
|
785
|
+
|
|
786
|
+
assert self.gpu_connector is not None, (
|
|
787
|
+
"gpu_connector is required for retrieve operation"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Get req_id for logging
|
|
791
|
+
req_id = self._get_req_id(kwargs)
|
|
792
|
+
|
|
793
|
+
tot_kv_size = 0
|
|
794
|
+
|
|
795
|
+
if mask is not None:
|
|
796
|
+
num_required_tokens = torch.sum(mask).item()
|
|
797
|
+
else:
|
|
798
|
+
num_required_tokens = len(tokens)
|
|
799
|
+
|
|
800
|
+
# KVCache Check logging
|
|
801
|
+
self._log_kvcache_for_check(
|
|
802
|
+
operation="retrieve",
|
|
803
|
+
kwargs=kwargs,
|
|
804
|
+
token_count=num_required_tokens,
|
|
805
|
+
require_req_id=True,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
retrieve_stats = self.stats_monitor.on_retrieve_request(num_required_tokens)
|
|
809
|
+
|
|
810
|
+
ret_mask = torch.zeros(len(tokens), dtype=torch.bool, device="cpu")
|
|
811
|
+
|
|
812
|
+
reordered_chunks: List[ProcessedChunk] = []
|
|
813
|
+
if not self._is_passive():
|
|
814
|
+
with retrieve_stats.profile_process_tokens():
|
|
815
|
+
if self.async_loading:
|
|
816
|
+
reordered_chunks, tot_kv_size = self._async_process_tokens_internal( # noqa: E501
|
|
817
|
+
tokens,
|
|
818
|
+
mask,
|
|
819
|
+
ret_mask,
|
|
820
|
+
**kwargs,
|
|
821
|
+
)
|
|
822
|
+
else:
|
|
823
|
+
reordered_chunks, tot_kv_size = self._process_tokens_internal(
|
|
824
|
+
tokens,
|
|
825
|
+
mask,
|
|
826
|
+
ret_mask,
|
|
827
|
+
**kwargs,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
if self.save_only_first_rank:
|
|
831
|
+
with retrieve_stats.profile_broadcast():
|
|
832
|
+
with torch.cuda.stream(self.broadcast_stream):
|
|
833
|
+
self._broadcast_or_receive_memory_objs(
|
|
834
|
+
reordered_chunks,
|
|
835
|
+
ret_mask,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# if self.gpu_connector has load_stream, self.broadcast_stream is equals
|
|
839
|
+
# to self.gpu_connector.load_stream, the broadcast and to_gpu operation
|
|
840
|
+
# will execute sequentially within the stream.
|
|
841
|
+
# if self.gpu_connector does not have load_stream, self.broadcast_stream
|
|
842
|
+
# is created by torch.cuda.Stream(), we need to synchronize broadcast
|
|
843
|
+
# operation, and then process to_cpu operation.
|
|
844
|
+
if not hasattr(self.gpu_connector, "load_stream"):
|
|
845
|
+
self.broadcast_stream.synchronize()
|
|
846
|
+
|
|
847
|
+
# NOTE(Jiayi): memory_obj doesn't have to be a pinned
|
|
848
|
+
# cpu tensor for the sake of performance.
|
|
849
|
+
# For example, disk->gpu is faster than disk->cpu->gpu.
|
|
850
|
+
# RDMA is another example.
|
|
851
|
+
if len(reordered_chunks) > 0:
|
|
852
|
+
with retrieve_stats.profile_to_gpu():
|
|
853
|
+
_, memory_objs, starts, ends = zip(*reordered_chunks, strict=False)
|
|
854
|
+
self.gpu_connector.batched_to_gpu(
|
|
855
|
+
list(memory_objs), list(starts), list(ends), **kwargs
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# TODO(Jiayi): Remove the following for loop with batched operations
|
|
859
|
+
# TODO(Jiayi): Need to refactor the `remove_after_retrieve` logic.
|
|
860
|
+
for key, memory_obj, _, _ in reordered_chunks:
|
|
861
|
+
if self.remove_after_retrieve and not self._is_passive():
|
|
862
|
+
assert self.storage_manager is not None
|
|
863
|
+
self.storage_manager.remove(key, self.retrieve_locations)
|
|
864
|
+
if not self.async_loading:
|
|
865
|
+
memory_obj.ref_count_down()
|
|
866
|
+
|
|
867
|
+
retrieved_tokens = torch.sum(ret_mask)
|
|
868
|
+
self.stats_monitor.on_retrieve_finished(
|
|
869
|
+
retrieve_stats,
|
|
870
|
+
retrieved_tokens,
|
|
871
|
+
)
|
|
872
|
+
onload_time = retrieve_stats.time_to_retrieve()
|
|
873
|
+
# The retrieved may be larger than the need_to_load
|
|
874
|
+
# Example (page_size=16, chunk_size=256):
|
|
875
|
+
#
|
|
876
|
+
# chunks: [0..255] [256..511]
|
|
877
|
+
# pages: [0..15]...[240..255] [256..271][272..287] ...
|
|
878
|
+
#
|
|
879
|
+
# num_computed_tokens = 288 => vLLM already has [0..287] (18 pages)
|
|
880
|
+
# LMCache hit_prefix_tokens = 512 => cache covers [0..511] (2 chunks)
|
|
881
|
+
#
|
|
882
|
+
# Skip chunk 1, retrieve chunk 2, overwrite [256..287] (32-token overlap)
|
|
883
|
+
# need_to_load: 512 - 288 = 224 tokens
|
|
884
|
+
# retrieved: 256 tokens
|
|
885
|
+
if not self._is_passive():
|
|
886
|
+
logger.info(
|
|
887
|
+
"[req_id=%s] Retrieved %d out of %d required tokens "
|
|
888
|
+
"(from %d total tokens). size: %.4f gb, "
|
|
889
|
+
"cost %.4f ms, throughput: %.4f GB/s;",
|
|
890
|
+
req_id,
|
|
891
|
+
retrieved_tokens,
|
|
892
|
+
num_required_tokens,
|
|
893
|
+
len(tokens),
|
|
894
|
+
tot_kv_size / 1024**3,
|
|
895
|
+
onload_time * 1000,
|
|
896
|
+
tot_kv_size / onload_time / 1024**3 if onload_time > 0 else 0,
|
|
897
|
+
)
|
|
898
|
+
return ret_mask
|
|
899
|
+
|
|
900
|
+
@_lmcache_nvtx_annotate
|
|
901
|
+
@torch.inference_mode()
|
|
902
|
+
def retrieve_layer(
|
|
903
|
+
self,
|
|
904
|
+
tokens: Union[torch.Tensor, list[int]],
|
|
905
|
+
mask: Optional[torch.Tensor] = None,
|
|
906
|
+
**kwargs,
|
|
907
|
+
) -> Generator[Optional[torch.Tensor], None, None]:
|
|
908
|
+
"""
|
|
909
|
+
Retrieve the KV cache in a layerwise manner.
|
|
910
|
+
|
|
911
|
+
:param torch.Tensor tokens: The tokens of the corresponding KV caches.
|
|
912
|
+
|
|
913
|
+
:param Optional[torch.Tensor] mask: The mask for the tokens. Should
|
|
914
|
+
have the same length as tokens. And the mask should ALWAYS be like
|
|
915
|
+
FFFFFTTTTTTT, where True means the tokens needs to be matched.
|
|
916
|
+
|
|
917
|
+
:param **kwargs: The additional arguments for the storage backend which
|
|
918
|
+
will be passed into the gpu_connector.
|
|
919
|
+
|
|
920
|
+
return: A generator that yields Optional[torch.Tensor]. The tensor will
|
|
921
|
+
be the boolean mask indicating which tokens are retrieved and will
|
|
922
|
+
only be returned in the last iteration. In the first iteration,
|
|
923
|
+
the generator retrieve the memory objects of the first layer from
|
|
924
|
+
the storage backends. In the next iterations, it moves the KV cache
|
|
925
|
+
of layer i from the memory objects (on CPU) to GPU and retrieves
|
|
926
|
+
the memory objects of layer i+1 from the storage backends. In the
|
|
927
|
+
last iteration, it moves the memory objects of the last layer to
|
|
928
|
+
the GPU.
|
|
929
|
+
"""
|
|
930
|
+
# Health check: block operation if LMCache is unhealthy
|
|
931
|
+
if not self.is_healthy():
|
|
932
|
+
logger.warning("LMCache is unhealthy, skipping retrieve_layer operation")
|
|
933
|
+
yield torch.zeros(len(tokens), dtype=torch.bool)
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
assert self.storage_manager is not None
|
|
937
|
+
assert self.gpu_connector is not None, (
|
|
938
|
+
"gpu_connector is required for retrieve_layer operation"
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
# Get req_id for logging
|
|
942
|
+
req_id = self._get_req_id(kwargs)
|
|
943
|
+
|
|
944
|
+
if mask is not None:
|
|
945
|
+
num_required_tokens = torch.sum(mask).item()
|
|
946
|
+
else:
|
|
947
|
+
num_required_tokens = len(tokens)
|
|
948
|
+
monitor_req_id = self.stats_monitor.on_retrieve_request(num_required_tokens)
|
|
949
|
+
|
|
950
|
+
ret_mask = torch.zeros(len(tokens), dtype=torch.bool, device="cpu")
|
|
951
|
+
|
|
952
|
+
starts = []
|
|
953
|
+
ends = []
|
|
954
|
+
keys = []
|
|
955
|
+
|
|
956
|
+
request_configs = kwargs.get("request_configs")
|
|
957
|
+
if request_configs is not None and len(request_configs) != 0:
|
|
958
|
+
assert isinstance(request_configs, dict)
|
|
959
|
+
|
|
960
|
+
location = None
|
|
961
|
+
for start, end, key in self.token_database.process_tokens(
|
|
962
|
+
tokens=tokens,
|
|
963
|
+
mask=mask,
|
|
964
|
+
request_configs=request_configs,
|
|
965
|
+
):
|
|
966
|
+
assert isinstance(key, CacheEngineKey)
|
|
967
|
+
|
|
968
|
+
keys_multi_layer = key.split_layers(self.num_layers)
|
|
969
|
+
|
|
970
|
+
# NOTE: Only check the first layer
|
|
971
|
+
if current_location := self.storage_manager.contains(
|
|
972
|
+
keys_multi_layer[0], self.retrieve_locations
|
|
973
|
+
):
|
|
974
|
+
if location is None:
|
|
975
|
+
location = current_location
|
|
976
|
+
else:
|
|
977
|
+
# TODO(Jiayi): Support multi-location retrieval in the future
|
|
978
|
+
assert location == current_location, (
|
|
979
|
+
"All retrieved keys should be from the same location "
|
|
980
|
+
"when use layerwise retrieval."
|
|
981
|
+
"Please support multi-location retrieval in the future."
|
|
982
|
+
)
|
|
983
|
+
else:
|
|
984
|
+
break
|
|
985
|
+
|
|
986
|
+
starts.append(start)
|
|
987
|
+
ends.append(end)
|
|
988
|
+
keys.append(keys_multi_layer)
|
|
989
|
+
|
|
990
|
+
ret_mask[start:end] = True
|
|
991
|
+
|
|
992
|
+
if keys:
|
|
993
|
+
# Transpose the keys into layer major format
|
|
994
|
+
keys_layer_major = [list(row) for row in zip(*keys, strict=False)]
|
|
995
|
+
|
|
996
|
+
get_generator = self.storage_manager.layerwise_batched_get(
|
|
997
|
+
keys_layer_major,
|
|
998
|
+
location=location,
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
assert_layerwise_gpu_connector(self.gpu_connector)
|
|
1002
|
+
|
|
1003
|
+
mem_obj_consumer = self.gpu_connector.batched_to_gpu(starts, ends, **kwargs)
|
|
1004
|
+
next(mem_obj_consumer)
|
|
1005
|
+
|
|
1006
|
+
to_count_down = []
|
|
1007
|
+
for layer_id in range(self.num_layers):
|
|
1008
|
+
task = next(get_generator)
|
|
1009
|
+
|
|
1010
|
+
assert task is not None
|
|
1011
|
+
|
|
1012
|
+
if layer_id == 0:
|
|
1013
|
+
# NOTE(Yuwei): For sglang integration we need to provide retrieved
|
|
1014
|
+
# tokens number in the first layer loading since there is no lookup
|
|
1015
|
+
yield torch.sum(ret_mask)
|
|
1016
|
+
else:
|
|
1017
|
+
yield None
|
|
1018
|
+
|
|
1019
|
+
mem_objs_layer = task.result()
|
|
1020
|
+
mem_obj_consumer.send(mem_objs_layer)
|
|
1021
|
+
to_count_down.extend(mem_objs_layer)
|
|
1022
|
+
|
|
1023
|
+
for mem_obj in to_count_down:
|
|
1024
|
+
mem_obj.ref_count_down()
|
|
1025
|
+
else:
|
|
1026
|
+
# If no cache are found, we still need to yield to avoid
|
|
1027
|
+
# `StopIteration`
|
|
1028
|
+
for layer_id in range(self.num_layers):
|
|
1029
|
+
yield None
|
|
1030
|
+
|
|
1031
|
+
yield None
|
|
1032
|
+
|
|
1033
|
+
# synchronize the last layer
|
|
1034
|
+
next(mem_obj_consumer)
|
|
1035
|
+
|
|
1036
|
+
# Unpin any disk-loaded staging objects now that the device-side sync
|
|
1037
|
+
# has been enqueued (mem_obj_consumer advanced past its sync point).
|
|
1038
|
+
# Without this, pin_count stays at 1 forever and the CPU staging pool
|
|
1039
|
+
# fills up, causing the next retrieve to deadlock inside allocate().
|
|
1040
|
+
for mem_obj in to_count_down:
|
|
1041
|
+
if mem_obj.is_pinned:
|
|
1042
|
+
mem_obj.unpin()
|
|
1043
|
+
|
|
1044
|
+
retrieved_tokens = torch.sum(ret_mask)
|
|
1045
|
+
self.stats_monitor.on_retrieve_finished(monitor_req_id, retrieved_tokens)
|
|
1046
|
+
if not self._is_passive():
|
|
1047
|
+
logger.info(
|
|
1048
|
+
"[req_id=%s] Retrieved %d out of %d out of total %d tokens",
|
|
1049
|
+
req_id,
|
|
1050
|
+
retrieved_tokens,
|
|
1051
|
+
num_required_tokens,
|
|
1052
|
+
len(tokens),
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
yield ret_mask
|
|
1056
|
+
|
|
1057
|
+
@_lmcache_nvtx_annotate
|
|
1058
|
+
def lookup(
|
|
1059
|
+
self,
|
|
1060
|
+
tokens: Optional[Union[torch.Tensor, List[int]]] = None,
|
|
1061
|
+
hashes: Optional[List[int]] = None,
|
|
1062
|
+
offsets: Optional[List[int]] = None,
|
|
1063
|
+
search_range: Optional[List[str]] = None,
|
|
1064
|
+
lookup_id: Optional[str] = None,
|
|
1065
|
+
pin: bool = False,
|
|
1066
|
+
request_configs: Optional[dict] = None,
|
|
1067
|
+
) -> int:
|
|
1068
|
+
"""
|
|
1069
|
+
Checks the existence of KV cache of the tokens from the cache engine.
|
|
1070
|
+
|
|
1071
|
+
:param Optional[Union[torch.Tensor, List[int]]] tokens: the input tokens,
|
|
1072
|
+
with shape [seq_len]
|
|
1073
|
+
|
|
1074
|
+
:param Optional[List[int]] hashes: the input hashes, with length [num_chunks]
|
|
1075
|
+
:param Optional[List[int]] offsets: the offsets of each chunk,
|
|
1076
|
+
with length [num_chunks]
|
|
1077
|
+
|
|
1078
|
+
:param Optional[List[str]] search_range: The range of storage backends
|
|
1079
|
+
to search in. Should be a subset of
|
|
1080
|
+
["LocalCPUBackend", "LocalDiskBackend"] for now.
|
|
1081
|
+
If None, search in all backends.
|
|
1082
|
+
|
|
1083
|
+
:param Optional[str] lookup_id: The lookup ID to
|
|
1084
|
+
associate with the lookup. When pin is true, this argument is
|
|
1085
|
+
required to be not None.
|
|
1086
|
+
|
|
1087
|
+
:param bool pin: If True, pin the KV cache in the storage.
|
|
1088
|
+
|
|
1089
|
+
:param Optional[dict] request_configs: the configs of the request.
|
|
1090
|
+
|
|
1091
|
+
:return: An int indicating how many prefix tokens exist inside LMCache.
|
|
1092
|
+
"""
|
|
1093
|
+
# Health check: block operation if LMCache is unhealthy
|
|
1094
|
+
if not self.is_healthy():
|
|
1095
|
+
logger.warning("LMCache is unhealthy, skipping lookup operation")
|
|
1096
|
+
return 0
|
|
1097
|
+
|
|
1098
|
+
assert self.storage_manager is not None
|
|
1099
|
+
|
|
1100
|
+
if tokens is not None:
|
|
1101
|
+
lookup_stats = self.stats_monitor.on_lookup_request(len(tokens))
|
|
1102
|
+
else:
|
|
1103
|
+
assert offsets is not None
|
|
1104
|
+
assert hashes is not None
|
|
1105
|
+
lookup_stats = self.stats_monitor.on_lookup_request(sum(offsets))
|
|
1106
|
+
|
|
1107
|
+
if search_range is None:
|
|
1108
|
+
search_range = self.retrieve_locations
|
|
1109
|
+
|
|
1110
|
+
res = 0
|
|
1111
|
+
try:
|
|
1112
|
+
chunk_info_iterator = self.token_database.process_tokens(
|
|
1113
|
+
tokens=tokens,
|
|
1114
|
+
hashes=hashes,
|
|
1115
|
+
offsets=offsets,
|
|
1116
|
+
request_configs=request_configs,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
# TODO: support batched_contains when layerwise is enabled
|
|
1120
|
+
if self.use_layerwise:
|
|
1121
|
+
for start, end, key in chunk_info_iterator:
|
|
1122
|
+
assert isinstance(key, CacheEngineKey)
|
|
1123
|
+
|
|
1124
|
+
# TODO(Jiayi): Optimize by checking only the existence of the key
|
|
1125
|
+
# of one layer
|
|
1126
|
+
key_all_layers = key.split_layers(self.num_layers)
|
|
1127
|
+
|
|
1128
|
+
hit_chunks, block_mapping = self.storage_manager.batched_contains(
|
|
1129
|
+
key_all_layers, # type: ignore
|
|
1130
|
+
search_range,
|
|
1131
|
+
pin,
|
|
1132
|
+
)
|
|
1133
|
+
# Only all layers are hit and hit in one location,
|
|
1134
|
+
# we consider this key as a hit
|
|
1135
|
+
if hit_chunks == self.num_layers and len(block_mapping) == 1:
|
|
1136
|
+
if pin:
|
|
1137
|
+
assert lookup_id is not None, (
|
|
1138
|
+
"lookup_id is required when pin is True"
|
|
1139
|
+
)
|
|
1140
|
+
location = next(iter(block_mapping.keys()))
|
|
1141
|
+
self.lookup_pins[lookup_id][location].extend(key_all_layers)
|
|
1142
|
+
res = end
|
|
1143
|
+
continue
|
|
1144
|
+
return res
|
|
1145
|
+
else:
|
|
1146
|
+
chunk_info_list = []
|
|
1147
|
+
keys = []
|
|
1148
|
+
for chunk_info in chunk_info_iterator:
|
|
1149
|
+
assert isinstance(chunk_info[2], CacheEngineKey)
|
|
1150
|
+
start, end, _ = chunk_info
|
|
1151
|
+
chunk_info_list.append(chunk_info)
|
|
1152
|
+
# chunk_info contains (start, end, key)
|
|
1153
|
+
# chunk_info[2] is the key
|
|
1154
|
+
keys.append(chunk_info[2])
|
|
1155
|
+
# hit chunks by prefix matching
|
|
1156
|
+
hit_chunks, block_mapping = self.storage_manager.batched_contains(
|
|
1157
|
+
keys, search_range, pin
|
|
1158
|
+
)
|
|
1159
|
+
if pin and block_mapping:
|
|
1160
|
+
assert lookup_id is not None, (
|
|
1161
|
+
"lookup_id is required when pin is True"
|
|
1162
|
+
)
|
|
1163
|
+
self.lookup_pins[lookup_id] = block_mapping
|
|
1164
|
+
for idx, (start, end, key) in enumerate(chunk_info_list):
|
|
1165
|
+
if idx < hit_chunks:
|
|
1166
|
+
res = end
|
|
1167
|
+
continue
|
|
1168
|
+
return res
|
|
1169
|
+
|
|
1170
|
+
# all tokens where found, return the maximal end
|
|
1171
|
+
return res
|
|
1172
|
+
finally:
|
|
1173
|
+
self.stats_monitor.on_lookup_finished(lookup_stats, res)
|
|
1174
|
+
# vllm lookup sets pin to True
|
|
1175
|
+
if pin:
|
|
1176
|
+
# touch_cache is tightly coupled with batched_contains
|
|
1177
|
+
self.storage_manager.touch_cache()
|
|
1178
|
+
|
|
1179
|
+
@_lmcache_nvtx_annotate
|
|
1180
|
+
def move(
|
|
1181
|
+
self,
|
|
1182
|
+
tokens: Union[torch.Tensor, List[int]],
|
|
1183
|
+
old_position: str,
|
|
1184
|
+
new_position: tuple[str, str],
|
|
1185
|
+
event_id: str,
|
|
1186
|
+
do_copy: bool = True,
|
|
1187
|
+
) -> int:
|
|
1188
|
+
"""
|
|
1189
|
+
Perform cross-node move of the KV cache.
|
|
1190
|
+
"""
|
|
1191
|
+
assert self.storage_manager is not None
|
|
1192
|
+
|
|
1193
|
+
num_tokens = self.lookup(
|
|
1194
|
+
tokens,
|
|
1195
|
+
search_range=[old_position],
|
|
1196
|
+
lookup_id=event_id,
|
|
1197
|
+
pin=True,
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
if not num_tokens:
|
|
1201
|
+
logger.debug("Move is not performed as there are no tokens to move.")
|
|
1202
|
+
return 0
|
|
1203
|
+
|
|
1204
|
+
block_mapping = self.lookup_pins[event_id]
|
|
1205
|
+
assert len(block_mapping) == 1
|
|
1206
|
+
keys = block_mapping[old_position]
|
|
1207
|
+
|
|
1208
|
+
memory_objs = self.storage_manager.batched_get(
|
|
1209
|
+
keys=keys,
|
|
1210
|
+
location=old_position,
|
|
1211
|
+
)
|
|
1212
|
+
assert None not in memory_objs, "Failed to get memory objects to move"
|
|
1213
|
+
logger.debug(
|
|
1214
|
+
f"Trying to send {len(memory_objs)} memory objects to {new_position}"
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
# TODO: reduce loops
|
|
1218
|
+
token_dim = memory_objs[0].meta.fmt.token_dim() # type: ignore
|
|
1219
|
+
offsets = [m.meta.shape[token_dim] for m in memory_objs] # type: ignore
|
|
1220
|
+
|
|
1221
|
+
transfer_spec = {
|
|
1222
|
+
"target_peer_init_url": new_position[0],
|
|
1223
|
+
"offsets": offsets,
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
logger.info(self.storage_manager.storage_backends)
|
|
1227
|
+
p2p_backend = self.storage_manager.storage_backends["P2PBackend"]
|
|
1228
|
+
|
|
1229
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
1230
|
+
p2p_backend.async_batched_submit_put_task(
|
|
1231
|
+
keys,
|
|
1232
|
+
memory_objs, # type: ignore
|
|
1233
|
+
transfer_spec=transfer_spec,
|
|
1234
|
+
),
|
|
1235
|
+
self.storage_manager.loop,
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
future.result()
|
|
1239
|
+
|
|
1240
|
+
if not do_copy:
|
|
1241
|
+
self.storage_manager.batched_remove(keys, locations=[old_position])
|
|
1242
|
+
|
|
1243
|
+
logger.debug(f"Moving {num_tokens} token from {old_position} to {new_position}")
|
|
1244
|
+
return num_tokens
|
|
1245
|
+
|
|
1246
|
+
# TODO(Jiayi): Add layerwise support.
|
|
1247
|
+
@_lmcache_nvtx_annotate
|
|
1248
|
+
def async_lookup_and_prefetch(
|
|
1249
|
+
self,
|
|
1250
|
+
lookup_id: str,
|
|
1251
|
+
tokens: Optional[Union[torch.Tensor, List[int]]] = None,
|
|
1252
|
+
hashes: Optional[List[int]] = None,
|
|
1253
|
+
offsets: Optional[List[int]] = None,
|
|
1254
|
+
search_range: Optional[List[str]] = None,
|
|
1255
|
+
pin: bool = False,
|
|
1256
|
+
request_configs: Optional[dict] = None,
|
|
1257
|
+
) -> None:
|
|
1258
|
+
"""
|
|
1259
|
+
An async version of lookup + prefetch.
|
|
1260
|
+
|
|
1261
|
+
There are three categories of backends:
|
|
1262
|
+
(1) sync lookup + sync retrieval (e.g., cpu)
|
|
1263
|
+
(2) sync lookup + async retrieval (e.g., disk)
|
|
1264
|
+
(3) async lookup + async retrieval (e.g., p2p)
|
|
1265
|
+
"""
|
|
1266
|
+
assert self.storage_manager is not None
|
|
1267
|
+
|
|
1268
|
+
keys: list[CacheEngineKey] = []
|
|
1269
|
+
cum_chunk_lengths = [0]
|
|
1270
|
+
|
|
1271
|
+
if search_range is None:
|
|
1272
|
+
search_range = self.retrieve_locations
|
|
1273
|
+
|
|
1274
|
+
# TODO(Jiayi): make token database able to return list.
|
|
1275
|
+
for start, end, key in self.token_database.process_tokens(
|
|
1276
|
+
tokens=tokens,
|
|
1277
|
+
hashes=hashes,
|
|
1278
|
+
offsets=offsets,
|
|
1279
|
+
request_configs=request_configs,
|
|
1280
|
+
):
|
|
1281
|
+
assert isinstance(key, CacheEngineKey)
|
|
1282
|
+
keys.append(key)
|
|
1283
|
+
cum_chunk_lengths.append(end)
|
|
1284
|
+
|
|
1285
|
+
asyncio.run_coroutine_threadsafe(
|
|
1286
|
+
self.storage_manager.async_lookup_and_prefetch(
|
|
1287
|
+
lookup_id, keys, cum_chunk_lengths, search_range, pin
|
|
1288
|
+
),
|
|
1289
|
+
self.storage_manager.loop,
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
def cleanup_memory_objs(self, lookup_id: str) -> None:
|
|
1293
|
+
"""
|
|
1294
|
+
Cleanup memory objects allocated during prefetch for an aborted lookup.
|
|
1295
|
+
|
|
1296
|
+
Called by the scheduler when it determines that an aborted lookup
|
|
1297
|
+
has finished its prefetch tasks.
|
|
1298
|
+
"""
|
|
1299
|
+
try:
|
|
1300
|
+
# Get the completed future from event_manager
|
|
1301
|
+
if (
|
|
1302
|
+
self.event_manager.get_event_status(EventType.LOADING, lookup_id)
|
|
1303
|
+
!= EventStatus.DONE
|
|
1304
|
+
):
|
|
1305
|
+
logger.debug(
|
|
1306
|
+
"No completed event found for lookup_id=%s to clean up.", lookup_id
|
|
1307
|
+
)
|
|
1308
|
+
return
|
|
1309
|
+
future = self.event_manager.pop_event(EventType.LOADING, lookup_id)
|
|
1310
|
+
|
|
1311
|
+
# Get memory objects from the future result
|
|
1312
|
+
memory_objs = future.result()
|
|
1313
|
+
# Flatten nested lists (each backend returns a list of chunks)
|
|
1314
|
+
memory_objs_flat = [mm for m in memory_objs for mm in m]
|
|
1315
|
+
|
|
1316
|
+
# Release each memory object
|
|
1317
|
+
for key, memory_obj in memory_objs_flat:
|
|
1318
|
+
try:
|
|
1319
|
+
logger.debug("Releasing memory object for lookup_id=%s", lookup_id)
|
|
1320
|
+
memory_obj.unpin()
|
|
1321
|
+
memory_obj.ref_count_down()
|
|
1322
|
+
except Exception as e:
|
|
1323
|
+
logger.error(f"Error releasing memory object: {e}")
|
|
1324
|
+
except Exception as e:
|
|
1325
|
+
logger.error(
|
|
1326
|
+
f"Error during cleanup_memory_objs for lookup_id={lookup_id}: {e}"
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
# TODO(Jiayi): Need to handle the case where `tokens=None`.
|
|
1330
|
+
# In this case, we compress all tokens.
|
|
1331
|
+
# TODO(Jiayi): support other compression methods.
|
|
1332
|
+
@_lmcache_nvtx_annotate
|
|
1333
|
+
def compress(
|
|
1334
|
+
self,
|
|
1335
|
+
tokens: Union[torch.Tensor, List[int]],
|
|
1336
|
+
method: str,
|
|
1337
|
+
location: str,
|
|
1338
|
+
event_id: str,
|
|
1339
|
+
) -> int:
|
|
1340
|
+
assert self.storage_manager is not None
|
|
1341
|
+
if method not in ["cachegen"]:
|
|
1342
|
+
logger.warning(f"Unsupported compression method: {method}.")
|
|
1343
|
+
return 0
|
|
1344
|
+
|
|
1345
|
+
# First Party
|
|
1346
|
+
from lmcache.v1.storage_backend.naive_serde import CreateSerde
|
|
1347
|
+
|
|
1348
|
+
serializer, _ = CreateSerde(method, self.metadata, self.config)
|
|
1349
|
+
|
|
1350
|
+
num_tokens = self.lookup(
|
|
1351
|
+
tokens,
|
|
1352
|
+
search_range=[location],
|
|
1353
|
+
lookup_id=event_id,
|
|
1354
|
+
pin=True,
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
if not num_tokens:
|
|
1358
|
+
logger.debug("Move is not performed as there are no tokens to move.")
|
|
1359
|
+
return 0
|
|
1360
|
+
|
|
1361
|
+
block_mapping = self.lookup_pins[event_id]
|
|
1362
|
+
assert len(block_mapping) == 1
|
|
1363
|
+
keys = block_mapping[location]
|
|
1364
|
+
|
|
1365
|
+
memory_objs = self.storage_manager.batched_get(
|
|
1366
|
+
keys=keys,
|
|
1367
|
+
location=location,
|
|
1368
|
+
)
|
|
1369
|
+
assert None not in memory_objs, (
|
|
1370
|
+
"LMCacheEngine.compress: Failed to get memory objects to compress"
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
compressed_memory_objs = []
|
|
1374
|
+
for memory_obj in memory_objs:
|
|
1375
|
+
assert memory_obj is not None
|
|
1376
|
+
compressed_memory_obj = serializer.serialize(memory_obj)
|
|
1377
|
+
memory_obj.unpin()
|
|
1378
|
+
compressed_memory_objs.append(compressed_memory_obj)
|
|
1379
|
+
|
|
1380
|
+
self.storage_manager.batched_remove(keys, locations=[location])
|
|
1381
|
+
|
|
1382
|
+
self.storage_manager.batched_put(
|
|
1383
|
+
keys=keys,
|
|
1384
|
+
memory_objs=compressed_memory_objs,
|
|
1385
|
+
location=location,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
return num_tokens
|
|
1389
|
+
|
|
1390
|
+
@_lmcache_nvtx_annotate
|
|
1391
|
+
def decompress(
|
|
1392
|
+
self,
|
|
1393
|
+
tokens: Union[torch.Tensor, List[int]],
|
|
1394
|
+
method: str,
|
|
1395
|
+
location: str,
|
|
1396
|
+
event_id: str,
|
|
1397
|
+
) -> int:
|
|
1398
|
+
assert self.storage_manager is not None
|
|
1399
|
+
if method not in ["cachegen"]:
|
|
1400
|
+
logger.warning(f"Unsupported decompression method: {method}.")
|
|
1401
|
+
return 0
|
|
1402
|
+
|
|
1403
|
+
# First Party
|
|
1404
|
+
from lmcache.v1.storage_backend.naive_serde import CreateSerde
|
|
1405
|
+
|
|
1406
|
+
_, deserializer = CreateSerde(method, self.metadata, self.config)
|
|
1407
|
+
|
|
1408
|
+
num_tokens = self.lookup(
|
|
1409
|
+
tokens,
|
|
1410
|
+
search_range=[location],
|
|
1411
|
+
lookup_id=event_id,
|
|
1412
|
+
pin=True,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
if not num_tokens:
|
|
1416
|
+
logger.debug("there are no tokens to decompress.")
|
|
1417
|
+
return 0
|
|
1418
|
+
|
|
1419
|
+
block_mapping = self.lookup_pins[event_id]
|
|
1420
|
+
assert len(block_mapping) == 1
|
|
1421
|
+
keys = block_mapping[location]
|
|
1422
|
+
|
|
1423
|
+
compressed_memory_objs = self.storage_manager.batched_get(
|
|
1424
|
+
keys=keys,
|
|
1425
|
+
location=location,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
assert None not in compressed_memory_objs, (
|
|
1429
|
+
"LMCacheEngine.compress: Failed to get compressed "
|
|
1430
|
+
"memory objects to decompress"
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
memory_objs = []
|
|
1434
|
+
for compressed_memory_obj in compressed_memory_objs:
|
|
1435
|
+
assert compressed_memory_obj is not None
|
|
1436
|
+
memory_obj = deserializer.deserialize(compressed_memory_obj)
|
|
1437
|
+
compressed_memory_obj.unpin()
|
|
1438
|
+
memory_objs.append(memory_obj)
|
|
1439
|
+
|
|
1440
|
+
self.storage_manager.batched_remove(keys, locations=[location])
|
|
1441
|
+
|
|
1442
|
+
self.storage_manager.batched_put(
|
|
1443
|
+
keys=keys,
|
|
1444
|
+
memory_objs=memory_objs,
|
|
1445
|
+
location=location,
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
return num_tokens
|
|
1449
|
+
|
|
1450
|
+
@_lmcache_nvtx_annotate
|
|
1451
|
+
def lookup_unpin(self, lookup_id: str) -> None:
|
|
1452
|
+
if lookup_id in self.lookup_pins:
|
|
1453
|
+
assert self.storage_manager is not None
|
|
1454
|
+
for location, keys in self.lookup_pins.pop(lookup_id).items():
|
|
1455
|
+
self.storage_manager.batched_unpin(keys, [location])
|
|
1456
|
+
|
|
1457
|
+
elif (
|
|
1458
|
+
self.async_loading is not None
|
|
1459
|
+
and self.event_manager.get_event_status(EventType.LOADING, lookup_id)
|
|
1460
|
+
!= EventStatus.NOT_FOUND
|
|
1461
|
+
):
|
|
1462
|
+
self.cleanup_memory_objs(lookup_id)
|
|
1463
|
+
|
|
1464
|
+
@_lmcache_nvtx_annotate
|
|
1465
|
+
def clear(
|
|
1466
|
+
self,
|
|
1467
|
+
tokens: Optional[Union[torch.Tensor, List[int]]] = None,
|
|
1468
|
+
locations: Optional[List[str]] = None,
|
|
1469
|
+
request_configs: Optional[dict] = None,
|
|
1470
|
+
) -> int:
|
|
1471
|
+
# TODO: need to clear by request_configs
|
|
1472
|
+
if self.save_only_first_rank:
|
|
1473
|
+
if self.metadata.is_first_rank():
|
|
1474
|
+
num_removed = self._clear(tokens, locations, request_configs)
|
|
1475
|
+
return num_removed
|
|
1476
|
+
else:
|
|
1477
|
+
return 0
|
|
1478
|
+
return self._clear(tokens, locations, request_configs)
|
|
1479
|
+
|
|
1480
|
+
@_lmcache_nvtx_annotate
|
|
1481
|
+
def get_kv_events(self) -> Iterable[CacheStoreEvent]:
|
|
1482
|
+
if self.kv_events_enabled and (events := self.kv_events):
|
|
1483
|
+
self.kv_events = []
|
|
1484
|
+
return events
|
|
1485
|
+
return []
|
|
1486
|
+
|
|
1487
|
+
def _clear(
|
|
1488
|
+
self,
|
|
1489
|
+
tokens: Optional[Union[torch.Tensor, List[int]]] = None,
|
|
1490
|
+
locations: Optional[List[str]] = None,
|
|
1491
|
+
request_configs: Optional[dict] = None,
|
|
1492
|
+
) -> int:
|
|
1493
|
+
assert self.storage_manager is not None
|
|
1494
|
+
assert isinstance(self.storage_manager, StorageManager)
|
|
1495
|
+
# Clear all caches if tokens is None
|
|
1496
|
+
if tokens is None or len(tokens) == 0:
|
|
1497
|
+
num_cleared = self.storage_manager.clear(locations)
|
|
1498
|
+
return num_cleared
|
|
1499
|
+
|
|
1500
|
+
num_removed = 0
|
|
1501
|
+
# Only remove the caches for the given tokens
|
|
1502
|
+
for start, end, key in self.token_database.process_tokens(
|
|
1503
|
+
tokens=tokens, request_configs=request_configs
|
|
1504
|
+
):
|
|
1505
|
+
assert isinstance(key, CacheEngineKey)
|
|
1506
|
+
removed = self.storage_manager.remove(key, locations)
|
|
1507
|
+
num_removed += removed
|
|
1508
|
+
return num_removed
|
|
1509
|
+
|
|
1510
|
+
@_lmcache_nvtx_annotate
|
|
1511
|
+
def health(
|
|
1512
|
+
self,
|
|
1513
|
+
) -> int:
|
|
1514
|
+
"""
|
|
1515
|
+
Check the health of the cache engine.
|
|
1516
|
+
return: 0 if healthy, otherwise the error code
|
|
1517
|
+
"""
|
|
1518
|
+
assert self.storage_manager is not None
|
|
1519
|
+
return 0 if self.storage_manager.memcheck() else -1
|
|
1520
|
+
|
|
1521
|
+
def close(self) -> None:
|
|
1522
|
+
"""Close the cache engine and free all the resources"""
|
|
1523
|
+
logger.info("Closing LMCacheEngine...")
|
|
1524
|
+
|
|
1525
|
+
if self.lmcache_worker is not None:
|
|
1526
|
+
try:
|
|
1527
|
+
logger.info("Closing lmcache_worker...")
|
|
1528
|
+
self.lmcache_worker.close()
|
|
1529
|
+
logger.info("lmcache_worker closed successfully")
|
|
1530
|
+
except Exception as e:
|
|
1531
|
+
logger.error(f"Error closing lmcache_worker: {e}")
|
|
1532
|
+
|
|
1533
|
+
try:
|
|
1534
|
+
logger.info("Closing storage_manager...")
|
|
1535
|
+
if self.storage_manager is not None:
|
|
1536
|
+
self.storage_manager.close()
|
|
1537
|
+
logger.info("storage_manager closed successfully")
|
|
1538
|
+
except Exception as e:
|
|
1539
|
+
logger.error(f"Error closing storage_manager: {e}")
|
|
1540
|
+
|
|
1541
|
+
logger.info("LMCacheEngine closed.")
|
|
1542
|
+
|
|
1543
|
+
def _async_process_tokens_internal(
|
|
1544
|
+
self,
|
|
1545
|
+
tokens,
|
|
1546
|
+
mask,
|
|
1547
|
+
ret_mask,
|
|
1548
|
+
**kwargs,
|
|
1549
|
+
) -> ProcessTokensInternalResult:
|
|
1550
|
+
"""
|
|
1551
|
+
This function is used to get the memory objects from the event manager.
|
|
1552
|
+
|
|
1553
|
+
Args:
|
|
1554
|
+
tokens: Input tokens to process
|
|
1555
|
+
mask: Mask indicating valid token positions
|
|
1556
|
+
ret_mask: Output mask updated with cache hit positions
|
|
1557
|
+
**kwargs: Additional keyword arguments
|
|
1558
|
+
"""
|
|
1559
|
+
assert "req_id" in kwargs, "req_id is required for async loading"
|
|
1560
|
+
request_configs = kwargs.get("request_configs")
|
|
1561
|
+
if request_configs is not None and len(request_configs) != 0:
|
|
1562
|
+
assert isinstance(request_configs, dict)
|
|
1563
|
+
|
|
1564
|
+
tot_kv_size = 0
|
|
1565
|
+
chunks: List[ProcessedChunk] = []
|
|
1566
|
+
future = self.event_manager.get_event_future(
|
|
1567
|
+
EventType.LOADING, kwargs["req_id"]
|
|
1568
|
+
)
|
|
1569
|
+
# As mentioned in async_lookup_and_prefetch(), the future.result()
|
|
1570
|
+
# is key data pair for each chunk in each tier. So extract the key
|
|
1571
|
+
# and memory object pairs to memory_obj_map
|
|
1572
|
+
try:
|
|
1573
|
+
keyed_memory_objs = future.result()
|
|
1574
|
+
memory_obj_map: dict[CacheEngineKey, MemoryObj] = {}
|
|
1575
|
+
except Exception as e:
|
|
1576
|
+
logger.error(f"Error popping event for request {kwargs['req_id']}: {e}")
|
|
1577
|
+
return [], 0
|
|
1578
|
+
|
|
1579
|
+
for backend_results in keyed_memory_objs:
|
|
1580
|
+
for key, memory_obj in backend_results:
|
|
1581
|
+
memory_obj_map[key] = memory_obj
|
|
1582
|
+
|
|
1583
|
+
# TODO(Jiayi): hashing inside `process_tokens` can be skipped.
|
|
1584
|
+
used_keys: set[CacheEngineKey] = set()
|
|
1585
|
+
for start, end, key in self.token_database.process_tokens(
|
|
1586
|
+
tokens=tokens,
|
|
1587
|
+
mask=mask,
|
|
1588
|
+
request_configs=request_configs,
|
|
1589
|
+
):
|
|
1590
|
+
assert isinstance(key, CacheEngineKey)
|
|
1591
|
+
memory_obj = memory_obj_map.get(key)
|
|
1592
|
+
if memory_obj is None:
|
|
1593
|
+
# returned chunks are expected to be contiguous.
|
|
1594
|
+
# break at the first missing chunk.
|
|
1595
|
+
break
|
|
1596
|
+
chunks.append((key, memory_obj, start, end))
|
|
1597
|
+
tot_kv_size += memory_obj.get_size()
|
|
1598
|
+
ret_mask[start:end] = True
|
|
1599
|
+
used_keys.add(key)
|
|
1600
|
+
|
|
1601
|
+
# NOTE: free the memory objects that are not hit.
|
|
1602
|
+
for key, mem_obj in memory_obj_map.items():
|
|
1603
|
+
if key not in used_keys:
|
|
1604
|
+
mem_obj.ref_count_down()
|
|
1605
|
+
|
|
1606
|
+
return chunks, tot_kv_size
|
|
1607
|
+
|
|
1608
|
+
def _process_tokens_internal(
|
|
1609
|
+
self,
|
|
1610
|
+
tokens,
|
|
1611
|
+
mask,
|
|
1612
|
+
ret_mask,
|
|
1613
|
+
**kwargs,
|
|
1614
|
+
) -> ProcessTokensInternalResult:
|
|
1615
|
+
"""Process tokens and populate the reordered lists.
|
|
1616
|
+
|
|
1617
|
+
This function is used to process tokens and populate the reordered lists.
|
|
1618
|
+
|
|
1619
|
+
Args:
|
|
1620
|
+
tokens: Input tokens to process
|
|
1621
|
+
mask: Mask indicating valid token positions
|
|
1622
|
+
ret_mask: Output mask updated with cache hit positions
|
|
1623
|
+
**kwargs: Additional keyword arguments
|
|
1624
|
+
"""
|
|
1625
|
+
assert self.storage_manager is not None
|
|
1626
|
+
|
|
1627
|
+
tot_kv_size = 0
|
|
1628
|
+
reordered_chunks: List[ProcessedChunk] = []
|
|
1629
|
+
request_configs = kwargs.get("request_configs")
|
|
1630
|
+
if request_configs is not None and len(request_configs) != 0:
|
|
1631
|
+
assert isinstance(request_configs, dict)
|
|
1632
|
+
|
|
1633
|
+
chunk_infos = []
|
|
1634
|
+
for start, end, key in self.token_database.process_tokens(
|
|
1635
|
+
tokens=tokens,
|
|
1636
|
+
mask=mask,
|
|
1637
|
+
request_configs=request_configs,
|
|
1638
|
+
):
|
|
1639
|
+
assert isinstance(key, CacheEngineKey)
|
|
1640
|
+
chunk_infos.append((key, start, end))
|
|
1641
|
+
|
|
1642
|
+
# block_mapping: location -> [(CacheEngineKey, start, end)]
|
|
1643
|
+
if (
|
|
1644
|
+
"req_id" in kwargs
|
|
1645
|
+
and kwargs["req_id"] in self.lookup_pins
|
|
1646
|
+
and len(self.lookup_pins[kwargs["req_id"]]) == 1
|
|
1647
|
+
):
|
|
1648
|
+
location = next(iter(self.lookup_pins[kwargs["req_id"]].keys()))
|
|
1649
|
+
block_mapping = {location: chunk_infos}
|
|
1650
|
+
else:
|
|
1651
|
+
block_mapping = self.storage_manager.get_block_mapping(chunk_infos)
|
|
1652
|
+
|
|
1653
|
+
last_failed_block_start = None
|
|
1654
|
+
for location, blocks in block_mapping.items():
|
|
1655
|
+
keys = [key for key, _, _ in blocks]
|
|
1656
|
+
memory_objs = self.storage_manager.batched_get(
|
|
1657
|
+
keys=keys,
|
|
1658
|
+
location=location,
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
used_keys: set[CacheEngineKey] = set()
|
|
1662
|
+
for (key, start, end), memory_obj in zip(blocks, memory_objs, strict=False):
|
|
1663
|
+
if memory_obj is None:
|
|
1664
|
+
logger.warning(
|
|
1665
|
+
"The cache block is in the storage, but it can't be retrieved"
|
|
1666
|
+
)
|
|
1667
|
+
if (
|
|
1668
|
+
last_failed_block_start is None
|
|
1669
|
+
# The minimum value should be taken here to ensure that
|
|
1670
|
+
# the prefix keys are all consecutive successful.
|
|
1671
|
+
or last_failed_block_start > start
|
|
1672
|
+
):
|
|
1673
|
+
last_failed_block_start = start
|
|
1674
|
+
break
|
|
1675
|
+
reordered_chunks.append((key, memory_obj, start, end))
|
|
1676
|
+
tot_kv_size += memory_obj.get_size()
|
|
1677
|
+
ret_mask[start:end] = True
|
|
1678
|
+
used_keys.add(key)
|
|
1679
|
+
|
|
1680
|
+
for (key, _, _), memory_obj in zip(blocks, memory_objs, strict=False):
|
|
1681
|
+
if memory_obj is not None and key not in used_keys:
|
|
1682
|
+
logger.debug(
|
|
1683
|
+
"ref_count_down for %s of %s as the previous key failed",
|
|
1684
|
+
key,
|
|
1685
|
+
location,
|
|
1686
|
+
)
|
|
1687
|
+
memory_obj.ref_count_down()
|
|
1688
|
+
|
|
1689
|
+
if last_failed_block_start is not None:
|
|
1690
|
+
ret_mask[last_failed_block_start:] = False
|
|
1691
|
+
|
|
1692
|
+
for key, memory_obj, _, end in reordered_chunks:
|
|
1693
|
+
if end > last_failed_block_start:
|
|
1694
|
+
logger.debug(
|
|
1695
|
+
"ref_count_down for %s as it is truncated by "
|
|
1696
|
+
"last_failed_block_start %d",
|
|
1697
|
+
key,
|
|
1698
|
+
last_failed_block_start,
|
|
1699
|
+
)
|
|
1700
|
+
tot_kv_size -= memory_obj.get_size()
|
|
1701
|
+
memory_obj.ref_count_down()
|
|
1702
|
+
|
|
1703
|
+
reordered_chunks = [
|
|
1704
|
+
(key, memory_obj, start, end)
|
|
1705
|
+
for key, memory_obj, start, end in reordered_chunks
|
|
1706
|
+
if end <= last_failed_block_start
|
|
1707
|
+
]
|
|
1708
|
+
return reordered_chunks, tot_kv_size
|
|
1709
|
+
|
|
1710
|
+
def _broadcast_or_receive_memory_objs(
|
|
1711
|
+
self,
|
|
1712
|
+
reordered_chunks,
|
|
1713
|
+
ret_mask,
|
|
1714
|
+
):
|
|
1715
|
+
"""
|
|
1716
|
+
Handles broadcasting or receiving memory objects in a distributed environment.
|
|
1717
|
+
|
|
1718
|
+
This function implements the communication logic where:
|
|
1719
|
+
- The first rank (coordinator) broadcasts memory objects and metadata to others
|
|
1720
|
+
- Other ranks receive and reconstruct the memory objects
|
|
1721
|
+
|
|
1722
|
+
Parameters:
|
|
1723
|
+
reordered_chunks: List of tuples containing [key, memory object, start, end]
|
|
1724
|
+
ret_mask: Boolean mask indicating which positions have been processed
|
|
1725
|
+
|
|
1726
|
+
Side Effects:
|
|
1727
|
+
- On first rank:
|
|
1728
|
+
* Broadcasts chunk count and each chunk's combined metadata
|
|
1729
|
+
* Broadcasts tensor data
|
|
1730
|
+
- On other ranks:
|
|
1731
|
+
* Receives chunk data and populates reordered_chunks
|
|
1732
|
+
* Updates ret_mask to mark received positions as True
|
|
1733
|
+
"""
|
|
1734
|
+
if self.metadata.is_first_rank():
|
|
1735
|
+
# Broadcast total chunk count
|
|
1736
|
+
chunk_count = len(reordered_chunks)
|
|
1737
|
+
self.broadcast_object_fn(chunk_count, self.metadata.first_rank)
|
|
1738
|
+
|
|
1739
|
+
# Broadcast each chunk's data
|
|
1740
|
+
for key, memory_obj, start, end in reordered_chunks:
|
|
1741
|
+
# Combine (start, end) and metadata into single broadcast
|
|
1742
|
+
metadata_dict = memory_obj.metadata.to_dict()
|
|
1743
|
+
combined_metadata = (start, end, metadata_dict)
|
|
1744
|
+
self.broadcast_object_fn(combined_metadata, self.metadata.first_rank)
|
|
1745
|
+
|
|
1746
|
+
# Broadcast tensor data
|
|
1747
|
+
raw_tensor = memory_obj.raw_tensor
|
|
1748
|
+
assert raw_tensor is not None
|
|
1749
|
+
tensor_to_broadcast = raw_tensor.to(f"cuda:{self.metadata.worker_id}")
|
|
1750
|
+
self.broadcast_fn(tensor_to_broadcast, self.metadata.first_rank)
|
|
1751
|
+
else:
|
|
1752
|
+
# Receive total chunk count
|
|
1753
|
+
chunk_count = self.broadcast_object_fn(None, self.metadata.first_rank)
|
|
1754
|
+
if chunk_count is None:
|
|
1755
|
+
logger.warning(
|
|
1756
|
+
f"rank={self.metadata.worker_id} received None chunk_count"
|
|
1757
|
+
)
|
|
1758
|
+
return
|
|
1759
|
+
|
|
1760
|
+
# Fill reordered_chunks with received data
|
|
1761
|
+
for _ in range(chunk_count):
|
|
1762
|
+
# Receive combined metadata (start, end, metadata_dict)
|
|
1763
|
+
combined_metadata = self.broadcast_object_fn(
|
|
1764
|
+
None, self.metadata.first_rank
|
|
1765
|
+
)
|
|
1766
|
+
if combined_metadata is None:
|
|
1767
|
+
logger.warning(
|
|
1768
|
+
f"rank={self.metadata.worker_id} "
|
|
1769
|
+
"received None combined_metadata"
|
|
1770
|
+
)
|
|
1771
|
+
break
|
|
1772
|
+
start, end, metadata_dict = combined_metadata
|
|
1773
|
+
ret_mask[start:end] = True
|
|
1774
|
+
|
|
1775
|
+
# Create tensor and receive data
|
|
1776
|
+
metadata = MemoryObjMetadata.from_dict(metadata_dict)
|
|
1777
|
+
local_rank = self.metadata.worker_id % torch.cuda.device_count()
|
|
1778
|
+
raw_tensor = torch.empty(
|
|
1779
|
+
torch.Size([metadata.get_size()]),
|
|
1780
|
+
dtype=torch.uint8,
|
|
1781
|
+
device=f"cuda:{local_rank}",
|
|
1782
|
+
)
|
|
1783
|
+
self.broadcast_fn(raw_tensor, self.metadata.first_rank)
|
|
1784
|
+
|
|
1785
|
+
# Create temporary memory object (key not needed for other ranks)
|
|
1786
|
+
memory_obj = TensorMemoryObj(
|
|
1787
|
+
raw_data=raw_tensor, metadata=metadata, parent_allocator=None
|
|
1788
|
+
)
|
|
1789
|
+
reordered_chunks.append((None, memory_obj, start, end))
|
|
1790
|
+
|
|
1791
|
+
def _is_passive(self):
|
|
1792
|
+
"""
|
|
1793
|
+
A 'passive' CacheEngine means that the node itself will not store/retrieve
|
|
1794
|
+
the data directly, but from the "active" worker (i.e., rank 0 in MLA)
|
|
1795
|
+
"""
|
|
1796
|
+
return self.save_only_first_rank and not self.metadata.is_first_rank()
|
|
1797
|
+
|
|
1798
|
+
def _get_slot_mapping_list(
|
|
1799
|
+
self,
|
|
1800
|
+
slot_mapping: Optional[Union[torch.Tensor, List[int]]],
|
|
1801
|
+
) -> Optional[List[int]]:
|
|
1802
|
+
"""
|
|
1803
|
+
Convert slot_mapping to list if it's a tensor, otherwise return as is.
|
|
1804
|
+
|
|
1805
|
+
:param slot_mapping: The slot_mapping to convert,
|
|
1806
|
+
can be a torch.Tensor or List[int], or None
|
|
1807
|
+
:type slot_mapping: Optional[Union[torch.Tensor, List[int]]]
|
|
1808
|
+
:return: The slot_mapping as a List[int], or None if input is None
|
|
1809
|
+
:rtype: Optional[List[int]]
|
|
1810
|
+
"""
|
|
1811
|
+
if slot_mapping is None:
|
|
1812
|
+
return None
|
|
1813
|
+
if isinstance(slot_mapping, torch.Tensor):
|
|
1814
|
+
return slot_mapping.tolist()
|
|
1815
|
+
# At this point, slot_mapping must be List[int]
|
|
1816
|
+
return slot_mapping
|
|
1817
|
+
|
|
1818
|
+
def _log_kvcache_for_check(
|
|
1819
|
+
self,
|
|
1820
|
+
operation: str,
|
|
1821
|
+
kwargs: dict,
|
|
1822
|
+
token_count: int,
|
|
1823
|
+
require_req_id: bool = False,
|
|
1824
|
+
) -> None:
|
|
1825
|
+
"""
|
|
1826
|
+
Helper method to log KVCache Check information.
|
|
1827
|
+
|
|
1828
|
+
This method centralizes the KVCache Check logging logic that was
|
|
1829
|
+
duplicated in multiple methods.
|
|
1830
|
+
|
|
1831
|
+
Args:
|
|
1832
|
+
operation: The operation being performed (e.g., "Store", "retrieve")
|
|
1833
|
+
kwargs: The keyword arguments containing slot_mapping and req_id
|
|
1834
|
+
token_count: The number of tokens involved in the operation
|
|
1835
|
+
require_req_id: Whether req_id must be present (default: False)
|
|
1836
|
+
"""
|
|
1837
|
+
if not self.kvcache_check_log_enabled:
|
|
1838
|
+
return
|
|
1839
|
+
|
|
1840
|
+
slot_mapping = kwargs.get("slot_mapping")
|
|
1841
|
+
if slot_mapping is None:
|
|
1842
|
+
return
|
|
1843
|
+
|
|
1844
|
+
if require_req_id:
|
|
1845
|
+
req_id = kwargs.get("req_id")
|
|
1846
|
+
if req_id is None:
|
|
1847
|
+
return
|
|
1848
|
+
else:
|
|
1849
|
+
req_id = kwargs.get("req_id", "unspecified")
|
|
1850
|
+
|
|
1851
|
+
# Convert slot_mapping to list if it's a tensor
|
|
1852
|
+
slot_mapping_list = self._get_slot_mapping_list(slot_mapping)
|
|
1853
|
+
# slot_mapping_list should not be None when slot_mapping is not None
|
|
1854
|
+
assert slot_mapping_list is not None
|
|
1855
|
+
|
|
1856
|
+
logger.info(
|
|
1857
|
+
"[KVCache Check] %s request %s, tokens=%d, slot_mapping: %s",
|
|
1858
|
+
operation,
|
|
1859
|
+
req_id,
|
|
1860
|
+
token_count,
|
|
1861
|
+
compress_slot_mapping(slot_mapping_list),
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
class LMCacheEngineBuilder:
|
|
1866
|
+
_instances: Dict[str, LMCacheEngine] = {}
|
|
1867
|
+
_cfgs: Dict[str, LMCacheEngineConfig] = {}
|
|
1868
|
+
_metadatas: Dict[str, LMCacheMetadata] = {}
|
|
1869
|
+
_stat_loggers: Dict[str, LMCacheStatsLogger] = {}
|
|
1870
|
+
|
|
1871
|
+
# TODO(Jiayi): Please remove this helper function in the future.
|
|
1872
|
+
# Currently, it's only used for testing.
|
|
1873
|
+
@staticmethod
|
|
1874
|
+
def _Create_memory_allocator(
|
|
1875
|
+
config: LMCacheEngineConfig,
|
|
1876
|
+
metadata: LMCacheMetadata,
|
|
1877
|
+
numa_mapping: Optional[NUMAMapping] = None,
|
|
1878
|
+
) -> MemoryAllocatorInterface:
|
|
1879
|
+
# NOTE: should remove this function after fixing the unit tests:
|
|
1880
|
+
# raise RuntimeError("_Create_memory_allocator is deprecated!")
|
|
1881
|
+
extra_config = config.extra_config
|
|
1882
|
+
enable_nixl_storage = extra_config is not None and extra_config.get(
|
|
1883
|
+
"enable_nixl_storage"
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
if enable_nixl_storage:
|
|
1887
|
+
# TODO(Jiayi): weird to import from transfer utils.
|
|
1888
|
+
# First Party
|
|
1889
|
+
from lmcache.v1.transfer_channel.transfer_utils import (
|
|
1890
|
+
get_correct_device,
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
corrected_device = get_correct_device(
|
|
1894
|
+
config.nixl_buffer_device,
|
|
1895
|
+
metadata.worker_id,
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
buffer = torch.empty(
|
|
1899
|
+
config.nixl_buffer_size,
|
|
1900
|
+
dtype=torch.uint8,
|
|
1901
|
+
device=corrected_device,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
if corrected_device == "cpu":
|
|
1905
|
+
torch.cuda.cudart().cudaHostRegister(
|
|
1906
|
+
buffer.data_ptr(), config.nixl_buffer_size, 0
|
|
1907
|
+
)
|
|
1908
|
+
else:
|
|
1909
|
+
logger.info(f"Setting cuda device to {corrected_device} ")
|
|
1910
|
+
torch.cuda.set_device(corrected_device)
|
|
1911
|
+
|
|
1912
|
+
return PagedTensorMemoryAllocator(
|
|
1913
|
+
buffer,
|
|
1914
|
+
[torch.Size(metadata.kv_shape)],
|
|
1915
|
+
[metadata.kv_dtype],
|
|
1916
|
+
MemoryFormat.KV_2LTD,
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
if config.gds_path is not None:
|
|
1920
|
+
assert config.gds_buffer_size is not None
|
|
1921
|
+
return CuFileMemoryAllocator(config.gds_buffer_size * 1024**2)
|
|
1922
|
+
|
|
1923
|
+
max_local_cpu_size = config.max_local_cpu_size
|
|
1924
|
+
# save_only_first_rank only works when use mla
|
|
1925
|
+
save_only_first_rank = (
|
|
1926
|
+
config.get_extra_config_value("save_only_first_rank", metadata.use_mla)
|
|
1927
|
+
and metadata.use_mla
|
|
1928
|
+
)
|
|
1929
|
+
if save_only_first_rank and metadata.is_first_rank():
|
|
1930
|
+
# Only the first rank will save the cache,
|
|
1931
|
+
# so we need to set it lager than other ranks
|
|
1932
|
+
first_rank_max_local_cpu_size = (
|
|
1933
|
+
config.extra_config.get(
|
|
1934
|
+
"first_rank_max_local_cpu_size", max_local_cpu_size
|
|
1935
|
+
)
|
|
1936
|
+
if config.extra_config
|
|
1937
|
+
else max_local_cpu_size
|
|
1938
|
+
)
|
|
1939
|
+
return MixedMemoryAllocator(
|
|
1940
|
+
int(first_rank_max_local_cpu_size * 1024**3),
|
|
1941
|
+
numa_mapping=numa_mapping,
|
|
1942
|
+
)
|
|
1943
|
+
return MixedMemoryAllocator(
|
|
1944
|
+
int(max_local_cpu_size * 1024**3),
|
|
1945
|
+
numa_mapping=numa_mapping,
|
|
1946
|
+
)
|
|
1947
|
+
|
|
1948
|
+
@staticmethod
|
|
1949
|
+
def _Create_token_database(
|
|
1950
|
+
config: LMCacheEngineConfig,
|
|
1951
|
+
metadata: LMCacheMetadata,
|
|
1952
|
+
) -> TokenDatabase:
|
|
1953
|
+
if config.enable_blending:
|
|
1954
|
+
return SegmentTokenDatabase(config, metadata)
|
|
1955
|
+
return ChunkedTokenDatabase(config, metadata)
|
|
1956
|
+
|
|
1957
|
+
@classmethod
|
|
1958
|
+
def get_or_create(
|
|
1959
|
+
cls,
|
|
1960
|
+
instance_id: str,
|
|
1961
|
+
config: LMCacheEngineConfig,
|
|
1962
|
+
metadata: LMCacheMetadata,
|
|
1963
|
+
gpu_connector: Optional[GPUConnectorInterface],
|
|
1964
|
+
broadcast_fn: Callable[[torch.Tensor, int], None],
|
|
1965
|
+
broadcast_object_fn: Callable[[Any, int], Any],
|
|
1966
|
+
) -> LMCacheEngine:
|
|
1967
|
+
"""
|
|
1968
|
+
Builds a new LMCacheEngine instance if it doesn't already exist for the
|
|
1969
|
+
given ID.
|
|
1970
|
+
|
|
1971
|
+
raises: ValueError if the instance already exists with a different
|
|
1972
|
+
configuration.
|
|
1973
|
+
"""
|
|
1974
|
+
logger.info(f"Creating LMCacheEngine instance {instance_id}")
|
|
1975
|
+
if instance_id not in cls._instances:
|
|
1976
|
+
numa_mapping = NUMADetector.get_numa_mapping(config)
|
|
1977
|
+
logger.info(f"NUMA mapping for instance {instance_id}: {numa_mapping}")
|
|
1978
|
+
token_database = cls._Create_token_database(config, metadata)
|
|
1979
|
+
stat_logger = LMCacheStatsLogger(
|
|
1980
|
+
metadata,
|
|
1981
|
+
log_interval=10,
|
|
1982
|
+
config=config,
|
|
1983
|
+
)
|
|
1984
|
+
|
|
1985
|
+
engine = LMCacheEngine(
|
|
1986
|
+
config,
|
|
1987
|
+
metadata,
|
|
1988
|
+
token_database,
|
|
1989
|
+
gpu_connector,
|
|
1990
|
+
broadcast_fn,
|
|
1991
|
+
broadcast_object_fn,
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
cls._instances[instance_id] = engine
|
|
1995
|
+
cls._cfgs[instance_id] = config
|
|
1996
|
+
cls._metadatas[instance_id] = metadata
|
|
1997
|
+
cls._stat_loggers[instance_id] = stat_logger
|
|
1998
|
+
return engine
|
|
1999
|
+
else:
|
|
2000
|
+
if (
|
|
2001
|
+
cls._cfgs[instance_id] != config
|
|
2002
|
+
or cls._metadatas[instance_id] != metadata
|
|
2003
|
+
):
|
|
2004
|
+
raise ValueError(
|
|
2005
|
+
f"Instance {instance_id} already exists with a different "
|
|
2006
|
+
f"configuration or metadata."
|
|
2007
|
+
)
|
|
2008
|
+
return cls._instances[instance_id]
|
|
2009
|
+
|
|
2010
|
+
@classmethod
|
|
2011
|
+
def get(cls, instance_id: str) -> Optional[LMCacheEngine]:
|
|
2012
|
+
"""Returns the LMCacheEngine instance associated with the instance ID,
|
|
2013
|
+
or None if not found."""
|
|
2014
|
+
return cls._instances.get(instance_id)
|
|
2015
|
+
|
|
2016
|
+
@classmethod
|
|
2017
|
+
def destroy(cls, instance_id: str) -> None:
|
|
2018
|
+
"""Close and delete the LMCacheEngine instance by the instance ID"""
|
|
2019
|
+
# TODO: unit test for this
|
|
2020
|
+
logger.info(f"Destroying LMCacheEngine instance: {instance_id}")
|
|
2021
|
+
|
|
2022
|
+
if instance_id in cls._instances:
|
|
2023
|
+
stat_logger = cls._stat_loggers[instance_id]
|
|
2024
|
+
try:
|
|
2025
|
+
logger.info("Shutting down stats logger...")
|
|
2026
|
+
stat_logger.shutdown()
|
|
2027
|
+
logger.info("Stats logger shut down successfully")
|
|
2028
|
+
except Exception as e:
|
|
2029
|
+
logger.error(f"Error shutting down stats logger: {e}")
|
|
2030
|
+
|
|
2031
|
+
engine = cls._instances[instance_id]
|
|
2032
|
+
try:
|
|
2033
|
+
logger.info("Closing cache engine...")
|
|
2034
|
+
engine.close()
|
|
2035
|
+
logger.info("Cache engine closed successfully")
|
|
2036
|
+
except Exception as e:
|
|
2037
|
+
logger.error(f"Error closing cache engine: {e}")
|
|
2038
|
+
|
|
2039
|
+
try:
|
|
2040
|
+
logger.info("Cleaning up instance dictionaries...")
|
|
2041
|
+
cls._instances.pop(instance_id, None)
|
|
2042
|
+
cls._cfgs.pop(instance_id, None)
|
|
2043
|
+
cls._metadatas.pop(instance_id, None)
|
|
2044
|
+
cls._stat_loggers.pop(instance_id, None)
|
|
2045
|
+
logger.info("Instance dictionaries cleaned up")
|
|
2046
|
+
except Exception as e:
|
|
2047
|
+
logger.error(f"Error cleaning up instances: {e}")
|
|
2048
|
+
|
|
2049
|
+
try:
|
|
2050
|
+
logger.info("Destroying stats monitor...")
|
|
2051
|
+
LMCStatsMonitor.DestroyInstance()
|
|
2052
|
+
logger.info("Stats monitor destroyed successfully")
|
|
2053
|
+
except Exception as e:
|
|
2054
|
+
logger.error(f"Error destroying stats monitor: {e}")
|
|
2055
|
+
|
|
2056
|
+
logger.info(f"LMCacheEngine instance {instance_id} destroyed")
|
|
2057
|
+
else:
|
|
2058
|
+
logger.warning(f"Instance {instance_id} not found for destruction")
|