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,1199 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Standard
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
5
|
+
from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
|
|
6
|
+
import asyncio
|
|
7
|
+
import ctypes
|
|
8
|
+
import json
|
|
9
|
+
import mmap
|
|
10
|
+
import os
|
|
11
|
+
import struct
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
# Third Party
|
|
18
|
+
import aiofile
|
|
19
|
+
import numpy as np
|
|
20
|
+
import torch
|
|
21
|
+
|
|
22
|
+
# First Party
|
|
23
|
+
from lmcache.logging import init_logger
|
|
24
|
+
from lmcache.utils import CacheEngineKey, DiskCacheMetadata, _lmcache_nvtx_annotate
|
|
25
|
+
from lmcache.v1.config import LMCacheEngineConfig
|
|
26
|
+
from lmcache.v1.memory_management import (
|
|
27
|
+
CuFileMemoryAllocator,
|
|
28
|
+
HipFileMemoryAllocator,
|
|
29
|
+
MemoryFormat,
|
|
30
|
+
MemoryObj,
|
|
31
|
+
)
|
|
32
|
+
from lmcache.v1.metadata import LMCacheMetadata
|
|
33
|
+
from lmcache.v1.storage_backend.abstract_backend import AllocatorBackendInterface
|
|
34
|
+
from lmcache.v1.storage_backend.path_sharder import PathSharder
|
|
35
|
+
|
|
36
|
+
logger = init_logger(__name__)
|
|
37
|
+
|
|
38
|
+
_METADATA_FILE_SUFFIX = ".metadata"
|
|
39
|
+
_DATA_FILE_SUFFIX = ".kvcache.safetensors"
|
|
40
|
+
_WEKA_DATA_FILE_SUFFIX = ".weka1"
|
|
41
|
+
_METADATA_VERSION = 1
|
|
42
|
+
_METADATA_MAX_SIZE = 4096 # reserve 4K for metadata.
|
|
43
|
+
# TODO: It is possible to read this 4KB block without triggering read-ahead by
|
|
44
|
+
# various means.
|
|
45
|
+
_DEFAULT_THREAD_COUNT = 4
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UnsupportedMetadataVersion(Exception):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
torch_dtypes = {
|
|
53
|
+
torch.half: "F16",
|
|
54
|
+
torch.bfloat16: "BF16",
|
|
55
|
+
torch.float32: "F32",
|
|
56
|
+
torch.float64: "F64",
|
|
57
|
+
torch.uint8: "U8",
|
|
58
|
+
torch.uint16: "U16",
|
|
59
|
+
torch.uint32: "U32",
|
|
60
|
+
torch.uint64: "U64",
|
|
61
|
+
torch.int8: "I8",
|
|
62
|
+
torch.int16: "I16",
|
|
63
|
+
torch.int32: "I32",
|
|
64
|
+
torch.int64: "I64",
|
|
65
|
+
torch.float8_e4m3fn: "F8E4M3FN",
|
|
66
|
+
torch.float8_e5m2: "F8E5M2",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
torch_dtypes_inverse = dict([(v, k) for k, v in torch_dtypes.items()])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_fstype(path):
|
|
73
|
+
with open("/proc/mounts", "r") as f:
|
|
74
|
+
lines = f.readlines()
|
|
75
|
+
|
|
76
|
+
# Find the best matching mount point
|
|
77
|
+
best_match = ""
|
|
78
|
+
best_fstype = ""
|
|
79
|
+
for line in lines:
|
|
80
|
+
parts = line.split()
|
|
81
|
+
if len(parts) >= 3:
|
|
82
|
+
_, mount_point, fstype = parts[0], parts[1], parts[2]
|
|
83
|
+
if path.startswith(mount_point) and len(mount_point) > len(best_match):
|
|
84
|
+
best_match = mount_point
|
|
85
|
+
best_fstype = fstype
|
|
86
|
+
|
|
87
|
+
if not best_fstype:
|
|
88
|
+
raise RuntimeError(f"Unable to detect fstype for {path}")
|
|
89
|
+
|
|
90
|
+
return best_fstype
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def pack_metadata(tensor, fmt: MemoryFormat, **extra_metadata) -> bytes:
|
|
94
|
+
if tensor.dtype not in torch_dtypes:
|
|
95
|
+
raise RuntimeError(f"unhandled dtype {tensor.dtype}")
|
|
96
|
+
|
|
97
|
+
# Metadata
|
|
98
|
+
data_size = tensor.numel() * tensor.element_size()
|
|
99
|
+
tensor_meta = {
|
|
100
|
+
"dtype": torch_dtypes[tensor.dtype],
|
|
101
|
+
"shape": list(tensor.size()),
|
|
102
|
+
"data_offsets": [0, data_size],
|
|
103
|
+
"fmt": fmt.value,
|
|
104
|
+
"__metadata__": extra_metadata,
|
|
105
|
+
}
|
|
106
|
+
meta = {"kvcache": tensor_meta}
|
|
107
|
+
str_meta = json.dumps(meta).encode("utf-8")
|
|
108
|
+
meta_len = len(str_meta)
|
|
109
|
+
assert meta_len <= _METADATA_MAX_SIZE - 8
|
|
110
|
+
|
|
111
|
+
# Align to _METADATA_MAX_SIZE - 8
|
|
112
|
+
str_meta += b" " * (_METADATA_MAX_SIZE - 8 - meta_len)
|
|
113
|
+
|
|
114
|
+
# Pack it all up so it is sized _METADATA_MAX_SIZE exactly.
|
|
115
|
+
return struct.pack("<Q", len(str_meta)) + str_meta
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def unpack_metadata(buffer: bytes):
|
|
119
|
+
meta_len = struct.unpack("<Q", buffer[:8])[0]
|
|
120
|
+
|
|
121
|
+
str_meta = buffer[8 : 8 + meta_len]
|
|
122
|
+
json_meta = str_meta.rstrip(b" ")
|
|
123
|
+
|
|
124
|
+
meta = json.loads(json_meta.decode("utf-8"))
|
|
125
|
+
tensor_meta = meta["kvcache"]
|
|
126
|
+
|
|
127
|
+
shape = tensor_meta["shape"]
|
|
128
|
+
dtype_str = tensor_meta["dtype"]
|
|
129
|
+
data_offsets = tensor_meta["data_offsets"]
|
|
130
|
+
fmt = MemoryFormat(tensor_meta["fmt"])
|
|
131
|
+
|
|
132
|
+
nbytes = data_offsets[1] - data_offsets[0]
|
|
133
|
+
dtype = torch_dtypes_inverse[dtype_str]
|
|
134
|
+
|
|
135
|
+
return torch.Size(shape), dtype, nbytes, fmt, tensor_meta["__metadata__"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def rand_suffix(n: int):
|
|
139
|
+
# Generates a random UUID hex string (e.g. "a8098c1a")
|
|
140
|
+
return uuid.uuid4().hex[:n]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def save_metadata(path: str, tmp: str, metadata: bytes):
|
|
144
|
+
tmp_path = path + tmp
|
|
145
|
+
async with aiofile.async_open(tmp_path, "wb") as f:
|
|
146
|
+
await f.write(metadata)
|
|
147
|
+
os.rename(tmp_path, path)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_extra_config_bool(key, config: LMCacheEngineConfig) -> bool | None:
|
|
151
|
+
"""Extract a boolean value from the config's extra_config dict.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
key: The key to look up in extra_config.
|
|
155
|
+
config: The LMCacheEngineConfig instance.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The boolean value if present, or None if not set.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
RuntimeError: If the value is not a valid boolean representation.
|
|
162
|
+
"""
|
|
163
|
+
value = config.extra_config.get(key, None)
|
|
164
|
+
if value is None:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
if isinstance(value, str):
|
|
168
|
+
bool_value = value.lower() == "true"
|
|
169
|
+
elif value in [False, True]:
|
|
170
|
+
bool_value = value
|
|
171
|
+
else:
|
|
172
|
+
raise RuntimeError(f"Invalid value `{value}` for `{key}` in extra_config")
|
|
173
|
+
|
|
174
|
+
logger.info(f"Getting {key} = {bool_value} from extra_config")
|
|
175
|
+
return bool_value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class GdsBackend(AllocatorBackendInterface):
|
|
179
|
+
"""
|
|
180
|
+
Originally based on the open sourced WekaGdsBackend, this is a backend that
|
|
181
|
+
leverages GPU Direct Storage APIs to issue GDS requests directly to the
|
|
182
|
+
GDS-supported remote filesystem. In order to use it, users need to specify
|
|
183
|
+
`gds_path` and `gds_buffer_size` in their LMCache config.
|
|
184
|
+
|
|
185
|
+
The GDS library to use is controlled by the `gds_backend` config field
|
|
186
|
+
(default: ``"cufile"``). Setting ``use_gds=False`` disables the GDS API
|
|
187
|
+
and falls back to POSIX I/O via cudart.
|
|
188
|
+
|
|
189
|
+
Cache Directory Structure created by this Backend:
|
|
190
|
+
/{gds_path}/{first_level}/{second_level}/{data & metadata} This structure
|
|
191
|
+
is semi-arbitrary. We create two levels in the directory hierarchy to
|
|
192
|
+
parallelize loading the data during initialization in the Python code.
|
|
193
|
+
|
|
194
|
+
NOTE: If GPUDirect is not supported on that other filesystem, then the GDS
|
|
195
|
+
library will fall back to POSIX I/O.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
config: LMCacheEngineConfig,
|
|
201
|
+
metadata: LMCacheMetadata,
|
|
202
|
+
loop: asyncio.AbstractEventLoop,
|
|
203
|
+
dst_device: str = "cuda",
|
|
204
|
+
):
|
|
205
|
+
assert dst_device.startswith("cuda")
|
|
206
|
+
super().__init__(dst_device=dst_device)
|
|
207
|
+
|
|
208
|
+
self.config = config
|
|
209
|
+
self.loop = loop
|
|
210
|
+
self.dst_device = dst_device
|
|
211
|
+
|
|
212
|
+
assert config.gds_path is not None, "Need to specify gds_path for GdsBackend"
|
|
213
|
+
|
|
214
|
+
sharder = PathSharder(
|
|
215
|
+
raw_csv=config.gds_path,
|
|
216
|
+
strategy=config.gds_path_sharding,
|
|
217
|
+
dst_device=dst_device,
|
|
218
|
+
)
|
|
219
|
+
self.gds_paths = sharder.all_paths
|
|
220
|
+
self.gds_path = sharder.selected
|
|
221
|
+
self.fstype = get_fstype(self.gds_path)
|
|
222
|
+
|
|
223
|
+
# Log the fstype - this is useful in reports and varying optimizations
|
|
224
|
+
# based on the kind of fstype used.
|
|
225
|
+
logger.info(
|
|
226
|
+
f"GDS backend using fstype '{self.fstype}' on path '{self.gds_path}'"
|
|
227
|
+
f" ({len(self.gds_paths)} path(s) configured)"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.use_gds = config.use_gds
|
|
231
|
+
self.gds_backend = config.gds_backend
|
|
232
|
+
# _user_set_keys is populated by LMCacheEngineConfig when a field is
|
|
233
|
+
# explicitly provided via a config file, environment variable, or
|
|
234
|
+
# keyword argument — as opposed to falling back to its default value.
|
|
235
|
+
# We check it here so that the tmpfs/overlayfs auto-disable logic
|
|
236
|
+
# below can distinguish "the user never mentioned use_gds (default
|
|
237
|
+
# True)" from "the user explicitly wrote use_gds: true". In the
|
|
238
|
+
# first case we auto-disable GDS on unsupported filesystems; in the
|
|
239
|
+
# second we respect the user's explicit intent and leave it enabled.
|
|
240
|
+
user_set_keys: set[str] = getattr(config, "_user_set_keys", set())
|
|
241
|
+
use_gds_explicitly_set = "use_gds" in user_set_keys
|
|
242
|
+
|
|
243
|
+
# Now initialize the memory allocator
|
|
244
|
+
self.memory_allocator = self.initialize_allocator(config, metadata)
|
|
245
|
+
|
|
246
|
+
self.data_suffix = _DATA_FILE_SUFFIX
|
|
247
|
+
self._thread_pool = None
|
|
248
|
+
|
|
249
|
+
if self.fstype in ["tmpfs", "overlayfs"]:
|
|
250
|
+
# TODO: we can replace the auto-detection of unsupported GDS
|
|
251
|
+
# file systems by doing a small GDS API test on them. If a
|
|
252
|
+
# read/write test fails, we can fallback to not using GDS APIs.
|
|
253
|
+
if use_gds_explicitly_set:
|
|
254
|
+
logger.warning("No automatic disabling of GDS usage due to fstype")
|
|
255
|
+
else:
|
|
256
|
+
logger.info("Automatic disabling of GDS usage due to fstype")
|
|
257
|
+
self.use_gds = False
|
|
258
|
+
elif self.fstype == "wekafs":
|
|
259
|
+
logger.info("Weka filesystem detected, GDS usage is enforced")
|
|
260
|
+
assert self.use_gds
|
|
261
|
+
self.data_suffix = _WEKA_DATA_FILE_SUFFIX
|
|
262
|
+
|
|
263
|
+
# Always enable the thread pool for parallel I/O
|
|
264
|
+
self.use_thread_pool = self.use_gds
|
|
265
|
+
|
|
266
|
+
if self.use_thread_pool:
|
|
267
|
+
thread_count = _DEFAULT_THREAD_COUNT
|
|
268
|
+
if config.extra_config is not None:
|
|
269
|
+
thread_count = config.extra_config.get(
|
|
270
|
+
"gds_io_threads", _DEFAULT_THREAD_COUNT
|
|
271
|
+
)
|
|
272
|
+
self._thread_pool = ThreadPoolExecutor(
|
|
273
|
+
max_workers=thread_count, thread_name_prefix="gds-io"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if self.use_gds:
|
|
277
|
+
logger.info("Using GDS backend '%s'", self.gds_backend)
|
|
278
|
+
if self.gds_backend == "cufile":
|
|
279
|
+
# HACK(Jiayi): cufile import is buggy on some hardware
|
|
280
|
+
# (e.g., without GPUDirect), so it's temporarily put here.
|
|
281
|
+
# Third Party
|
|
282
|
+
import cufile
|
|
283
|
+
|
|
284
|
+
self.cudart = None
|
|
285
|
+
self.gds_module = cufile
|
|
286
|
+
self._gds_driver = self.gds_module.CuFileDriver()
|
|
287
|
+
elif self.gds_backend == "hipfile":
|
|
288
|
+
# HACK: hipfile import may be buggy on some hardware
|
|
289
|
+
# (e.g., without GPUDirect), so it's temporarily put here.
|
|
290
|
+
# Third Party
|
|
291
|
+
import hipfile
|
|
292
|
+
|
|
293
|
+
self.cudart = None
|
|
294
|
+
self.gds_module = hipfile
|
|
295
|
+
self._gds_driver = self.gds_module.CuFileDriver()
|
|
296
|
+
else:
|
|
297
|
+
raise ValueError(f"Unsupported gds_backend '{self.gds_backend}'")
|
|
298
|
+
else:
|
|
299
|
+
logger.info("GDS disabled, using POSIX fallback")
|
|
300
|
+
self.gds_module = None
|
|
301
|
+
self.cudart = ctypes.CDLL("libcudart.so")
|
|
302
|
+
|
|
303
|
+
self.use_direct_io = False
|
|
304
|
+
|
|
305
|
+
# Values for retrying allocations and loads in case of failures potentially
|
|
306
|
+
# due to memory pressure
|
|
307
|
+
self.max_alloc_attempts = (config.extra_config or {}).get(
|
|
308
|
+
"max_alloc_attempts", 10
|
|
309
|
+
)
|
|
310
|
+
self.alloc_attempt_delay_secs = (config.extra_config or {}).get(
|
|
311
|
+
"allocation_attempt_delay_secs", 0.1
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if config.extra_config is not None:
|
|
315
|
+
use_direct_io = get_extra_config_bool("use_direct_io", config)
|
|
316
|
+
if use_direct_io is not None:
|
|
317
|
+
self.use_direct_io = use_direct_io
|
|
318
|
+
|
|
319
|
+
for p in self.gds_paths:
|
|
320
|
+
os.makedirs(p, exist_ok=True)
|
|
321
|
+
|
|
322
|
+
self.stats = None # TODO: plug into LMCache Statistics
|
|
323
|
+
|
|
324
|
+
self.hot_lock = threading.Lock()
|
|
325
|
+
self.hot_cache: OrderedDict[CacheEngineKey, DiskCacheMetadata] = OrderedDict()
|
|
326
|
+
self.metadata_dirs: set[str] = set()
|
|
327
|
+
|
|
328
|
+
self.put_lock = threading.Lock()
|
|
329
|
+
self.put_tasks: set[CacheEngineKey] = set()
|
|
330
|
+
|
|
331
|
+
if hasattr(self.memory_allocator, "base_pointer"):
|
|
332
|
+
logger.debug(f"Using base pointer {self.memory_allocator.base_pointer}")
|
|
333
|
+
self.gds_base_pointer = self.memory_allocator.base_pointer
|
|
334
|
+
else:
|
|
335
|
+
logger.info("No base pointer found, GDS will use bounce buffers")
|
|
336
|
+
self.gds_base_pointer = None
|
|
337
|
+
self._scan_metadata_future = asyncio.run_coroutine_threadsafe(
|
|
338
|
+
self._scan_metadata(), self.loop
|
|
339
|
+
)
|
|
340
|
+
self.save_metadata_tasks: set[asyncio.Task] = set()
|
|
341
|
+
|
|
342
|
+
# flag for extra assertions to catch bugs but harm performance
|
|
343
|
+
self._debug_asserts = False
|
|
344
|
+
# flag to use O_NOATIME during metadata file read for performance improvement
|
|
345
|
+
self._use_noatime = True
|
|
346
|
+
|
|
347
|
+
async def _scan_metadata(self):
|
|
348
|
+
# TODO: even though we only run it once on startup, this is still
|
|
349
|
+
# not super scalable - test whether Rust code will be faster here, or
|
|
350
|
+
# whether we can serialize meta-data in groups for faster loading.
|
|
351
|
+
tasks = []
|
|
352
|
+
start = time.perf_counter()
|
|
353
|
+
for p in self.gds_paths:
|
|
354
|
+
with os.scandir(p) as it:
|
|
355
|
+
for entry in it:
|
|
356
|
+
if not entry.is_dir():
|
|
357
|
+
continue
|
|
358
|
+
l1_dir = os.path.basename(entry.name)
|
|
359
|
+
if len(l1_dir) != 2:
|
|
360
|
+
continue
|
|
361
|
+
tasks.append(
|
|
362
|
+
asyncio.to_thread(
|
|
363
|
+
self._scan_metadata_subdir,
|
|
364
|
+
os.path.join(p, l1_dir),
|
|
365
|
+
l1_dir,
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
# TODO: If Python 3.11+, can we use TaskGroup instead?
|
|
369
|
+
await asyncio.gather(*tasks)
|
|
370
|
+
end = time.perf_counter()
|
|
371
|
+
logger.info(
|
|
372
|
+
f"Read {len(self.hot_cache)} cache entries from persistent "
|
|
373
|
+
f"storage in {end - start:.2f} seconds"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _scan_metadata_subdir(self, path, l1_dir):
|
|
377
|
+
target_suffix = self.data_suffix + _METADATA_FILE_SUFFIX
|
|
378
|
+
with os.scandir(path) as it:
|
|
379
|
+
for entry in it:
|
|
380
|
+
if not entry.is_dir():
|
|
381
|
+
continue
|
|
382
|
+
l2_dir = os.path.basename(entry.name)
|
|
383
|
+
if len(l2_dir) != 2:
|
|
384
|
+
continue
|
|
385
|
+
with os.scandir(os.path.join(path, l2_dir)) as it2:
|
|
386
|
+
for fentry in it2:
|
|
387
|
+
if not fentry.is_file():
|
|
388
|
+
continue
|
|
389
|
+
if not fentry.name.endswith(target_suffix):
|
|
390
|
+
continue
|
|
391
|
+
filename = os.path.basename(fentry.name)
|
|
392
|
+
key_str = urllib.parse.unquote(filename[: -len(target_suffix)])
|
|
393
|
+
try:
|
|
394
|
+
key = CacheEngineKey.from_string(key_str)
|
|
395
|
+
except ValueError as e:
|
|
396
|
+
logger.error(
|
|
397
|
+
f"Filename {filename} can't be converted "
|
|
398
|
+
f"back into cache key: {e}"
|
|
399
|
+
)
|
|
400
|
+
continue
|
|
401
|
+
try:
|
|
402
|
+
self._read_metadata(key, fentry.path, l1_dir + l2_dir)
|
|
403
|
+
except UnsupportedMetadataVersion:
|
|
404
|
+
logger.error(
|
|
405
|
+
"Unsupported metadata version for "
|
|
406
|
+
f"{fentry.path}, ignoring"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def _read_metadata_info(self, filename: str):
|
|
410
|
+
# Use O_NOATIME to prevent updating access time and improve performance
|
|
411
|
+
# Instead of using Python's open() and read(), we use the OS's open() and
|
|
412
|
+
# read() because it is faster - the metadata file is small and we don't
|
|
413
|
+
# need any buffering.
|
|
414
|
+
# Additionally, we use O_NOATIME to improve performance
|
|
415
|
+
if self._use_noatime:
|
|
416
|
+
try:
|
|
417
|
+
fd = os.open(filename, os.O_RDONLY | os.O_NOATIME)
|
|
418
|
+
except (
|
|
419
|
+
# PermissionError: User doesn't own the file
|
|
420
|
+
# AttributeError: O_NOATIME not available on this platform
|
|
421
|
+
# OSError: Filesystem doesn't support O_NOATIME (EINVAL)
|
|
422
|
+
PermissionError,
|
|
423
|
+
AttributeError,
|
|
424
|
+
OSError,
|
|
425
|
+
): # fallback to normal open if O_NOATIME is not supported
|
|
426
|
+
self._use_noatime = False
|
|
427
|
+
logger.info(
|
|
428
|
+
"O_NOATIME flag not supported during metadata file read, "
|
|
429
|
+
"falling back to normal open"
|
|
430
|
+
)
|
|
431
|
+
fd = os.open(filename, os.O_RDONLY)
|
|
432
|
+
else:
|
|
433
|
+
fd = os.open(filename, os.O_RDONLY)
|
|
434
|
+
try:
|
|
435
|
+
buf = os.read(fd, _METADATA_MAX_SIZE)
|
|
436
|
+
finally:
|
|
437
|
+
os.close(fd)
|
|
438
|
+
return unpack_metadata(buf)
|
|
439
|
+
|
|
440
|
+
def _read_metadata(
|
|
441
|
+
self,
|
|
442
|
+
key: CacheEngineKey,
|
|
443
|
+
filename: str,
|
|
444
|
+
subdir_key: str,
|
|
445
|
+
):
|
|
446
|
+
shape, dtype, size, fmt, extra_metadata = self._read_metadata_info(filename)
|
|
447
|
+
if extra_metadata["lmcache_version"] != str(_METADATA_VERSION):
|
|
448
|
+
raise RuntimeError("unhandled lmcache metadata")
|
|
449
|
+
logger.debug(
|
|
450
|
+
f"Read metadata for {key} from {filename}: "
|
|
451
|
+
f"shape={shape}, dtype={dtype}, size={size}, fmt={fmt}, "
|
|
452
|
+
f"extra_metadata={extra_metadata}"
|
|
453
|
+
)
|
|
454
|
+
# TODO(extra_metadata)
|
|
455
|
+
# TODO(Jiayi): need to support `cached_positions`.
|
|
456
|
+
# Currently we just fill it as None.
|
|
457
|
+
metadata = DiskCacheMetadata(
|
|
458
|
+
filename.removesuffix(_METADATA_FILE_SUFFIX),
|
|
459
|
+
size,
|
|
460
|
+
shape,
|
|
461
|
+
dtype,
|
|
462
|
+
None,
|
|
463
|
+
fmt,
|
|
464
|
+
)
|
|
465
|
+
with self.hot_lock:
|
|
466
|
+
self.metadata_dirs.add(subdir_key)
|
|
467
|
+
self.hot_cache[key] = metadata
|
|
468
|
+
return metadata
|
|
469
|
+
|
|
470
|
+
def __str__(self):
|
|
471
|
+
return self.__class__.__name__
|
|
472
|
+
|
|
473
|
+
def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
|
|
474
|
+
# TODO: implement pin() semantics
|
|
475
|
+
with self.hot_lock:
|
|
476
|
+
res = key in self.hot_cache
|
|
477
|
+
if res:
|
|
478
|
+
return True
|
|
479
|
+
if self._try_to_read_metadata(key):
|
|
480
|
+
return True
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
def _try_to_read_metadata(self, key: CacheEngineKey) -> Optional[DiskCacheMetadata]:
|
|
484
|
+
for p in self.gds_paths:
|
|
485
|
+
path, subdir_key, _, _ = self._key_to_path(key, base_path=p)
|
|
486
|
+
path += _METADATA_FILE_SUFFIX
|
|
487
|
+
if os.path.exists(path):
|
|
488
|
+
try:
|
|
489
|
+
return self._read_metadata(key, path, subdir_key)
|
|
490
|
+
except FileNotFoundError:
|
|
491
|
+
logger.warning(
|
|
492
|
+
f"[GDS] File not found for key {key.to_string()} "
|
|
493
|
+
f"at expected path {path}, returning None"
|
|
494
|
+
)
|
|
495
|
+
except PermissionError:
|
|
496
|
+
logger.warning(
|
|
497
|
+
f"[GDS]: Permission Denied for PID {os.getpid()} on {path},"
|
|
498
|
+
f" returning None"
|
|
499
|
+
)
|
|
500
|
+
except UnsupportedMetadataVersion:
|
|
501
|
+
logger.error(f"Unsupported metadata version for {path}, ignoring")
|
|
502
|
+
except (OSError, IOError) as e:
|
|
503
|
+
logger.error(
|
|
504
|
+
f"Failed to read metadata file {path}: {type(e).__name__}: "
|
|
505
|
+
f"{e}. File may be corrupted or inaccessible. "
|
|
506
|
+
f"Ignoring cache entry for key {key.to_string()}."
|
|
507
|
+
)
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.error(
|
|
510
|
+
f"Unexpected error reading metadata file {path}: "
|
|
511
|
+
f"{type(e).__name__}: {e}. Ignoring cache entry for key "
|
|
512
|
+
f"{key.to_string()}."
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
def _key_to_path(
|
|
518
|
+
self,
|
|
519
|
+
key: CacheEngineKey,
|
|
520
|
+
base_path: Optional[str] = None,
|
|
521
|
+
) -> Tuple[str, str, str, str]:
|
|
522
|
+
hash = str(key.chunk_hash)
|
|
523
|
+
l1_dir = hash[:2]
|
|
524
|
+
l2_dir = hash[2:4]
|
|
525
|
+
key_str = key.to_string()
|
|
526
|
+
if base_path is None:
|
|
527
|
+
base_path = self.gds_path
|
|
528
|
+
return (
|
|
529
|
+
os.path.join(
|
|
530
|
+
base_path,
|
|
531
|
+
l1_dir,
|
|
532
|
+
l2_dir,
|
|
533
|
+
urllib.parse.quote(key_str, safe="") + self.data_suffix,
|
|
534
|
+
),
|
|
535
|
+
l1_dir + l2_dir,
|
|
536
|
+
l1_dir,
|
|
537
|
+
l2_dir,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
|
|
541
|
+
with self.put_lock:
|
|
542
|
+
return key in self.put_tasks
|
|
543
|
+
|
|
544
|
+
def submit_put_task(
|
|
545
|
+
self,
|
|
546
|
+
key: CacheEngineKey,
|
|
547
|
+
memory_obj: MemoryObj,
|
|
548
|
+
on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
|
|
549
|
+
) -> Future:
|
|
550
|
+
"""
|
|
551
|
+
Submit a put task to store KV cache to GDS asynchronously.
|
|
552
|
+
|
|
553
|
+
:param on_complete_callback: Optional callback invoked after the GDS
|
|
554
|
+
write completes. Callback exceptions are caught and logged.
|
|
555
|
+
"""
|
|
556
|
+
assert memory_obj.tensor is not None
|
|
557
|
+
memory_obj.ref_count_up()
|
|
558
|
+
|
|
559
|
+
with self.put_lock:
|
|
560
|
+
self.put_tasks.add(key)
|
|
561
|
+
|
|
562
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
563
|
+
self._async_save_bytes_to_disk(key, memory_obj, on_complete_callback),
|
|
564
|
+
self.loop,
|
|
565
|
+
)
|
|
566
|
+
return future
|
|
567
|
+
|
|
568
|
+
def batched_submit_put_task(
|
|
569
|
+
self,
|
|
570
|
+
keys: Sequence[CacheEngineKey],
|
|
571
|
+
memory_objs: List[MemoryObj],
|
|
572
|
+
transfer_spec: Any = None,
|
|
573
|
+
on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
|
|
574
|
+
) -> Union[List[Future], None]:
|
|
575
|
+
"""
|
|
576
|
+
Submit batched put tasks to store KV caches to GDS asynchronously.
|
|
577
|
+
|
|
578
|
+
:param on_complete_callback: Optional callback invoked once per key
|
|
579
|
+
after that key's write completes (not once per batch).
|
|
580
|
+
"""
|
|
581
|
+
futures = []
|
|
582
|
+
for key, memory_obj in zip(keys, memory_objs, strict=False):
|
|
583
|
+
future = self.submit_put_task(
|
|
584
|
+
key, memory_obj, on_complete_callback=on_complete_callback
|
|
585
|
+
)
|
|
586
|
+
futures.append(future)
|
|
587
|
+
return futures
|
|
588
|
+
|
|
589
|
+
async def _async_save_bytes_to_disk(
|
|
590
|
+
self,
|
|
591
|
+
key: CacheEngineKey,
|
|
592
|
+
memory_obj: MemoryObj,
|
|
593
|
+
on_complete_callback: Optional[Callable[[CacheEngineKey], None]] = None,
|
|
594
|
+
) -> None:
|
|
595
|
+
"""
|
|
596
|
+
Convert KV to bytes and async store bytes to disk.
|
|
597
|
+
|
|
598
|
+
:param on_complete_callback: Optional callback invoked after the GDS
|
|
599
|
+
write completes for this key. Callback exceptions are caught.
|
|
600
|
+
"""
|
|
601
|
+
try:
|
|
602
|
+
kv_chunk = memory_obj.tensor
|
|
603
|
+
assert kv_chunk is not None
|
|
604
|
+
path, subdir_key, l1_dir, l2_dir = self._key_to_path(key)
|
|
605
|
+
# TODO: maybe remove `metadata_dirs` and insert mkdir calls
|
|
606
|
+
# only for the case where creating the CuFile fails on ENOENT. It
|
|
607
|
+
# also makes the code more resilient to out-of-band deletions
|
|
608
|
+
if subdir_key not in self.metadata_dirs:
|
|
609
|
+
os.makedirs(os.path.join(self.gds_path, l1_dir, l2_dir), exist_ok=True)
|
|
610
|
+
self.metadata_dirs.add(subdir_key)
|
|
611
|
+
tmp = ".tmp" + rand_suffix(8)
|
|
612
|
+
fmt = memory_obj.metadata.fmt
|
|
613
|
+
try:
|
|
614
|
+
metadata = await asyncio.to_thread(
|
|
615
|
+
self._save_gds,
|
|
616
|
+
path,
|
|
617
|
+
tmp,
|
|
618
|
+
kv_chunk,
|
|
619
|
+
fmt,
|
|
620
|
+
self.gds_base_pointer,
|
|
621
|
+
memory_obj.metadata.address,
|
|
622
|
+
)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error(
|
|
625
|
+
f"GDS write operation failed for key {key.to_string()} at "
|
|
626
|
+
f"path {path}: tensor_shape={kv_chunk.shape}, "
|
|
627
|
+
f"tensor_dtype={kv_chunk.dtype}, "
|
|
628
|
+
f"tensor_size_bytes={kv_chunk.nbytes}, error={e}",
|
|
629
|
+
exc_info=True,
|
|
630
|
+
)
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
# Register key in cache
|
|
634
|
+
logger.debug(
|
|
635
|
+
f"Saved {kv_chunk.numel()} elements of {kv_chunk.dtype} "
|
|
636
|
+
f"to {path} with metadata {metadata}"
|
|
637
|
+
)
|
|
638
|
+
self.insert_key(key, memory_obj)
|
|
639
|
+
try:
|
|
640
|
+
task = asyncio.create_task(
|
|
641
|
+
save_metadata(path + _METADATA_FILE_SUFFIX, tmp, metadata)
|
|
642
|
+
)
|
|
643
|
+
self.save_metadata_tasks.add(task)
|
|
644
|
+
task.add_done_callback(self.save_metadata_tasks.discard)
|
|
645
|
+
# Add callback to check for exceptions during task execution
|
|
646
|
+
task.add_done_callback(
|
|
647
|
+
lambda t: self._handle_metadata_write_completion(t, key, path)
|
|
648
|
+
)
|
|
649
|
+
except Exception as e:
|
|
650
|
+
logger.error(
|
|
651
|
+
f"POSIX metadata write operation failed for key {key.to_string()} "
|
|
652
|
+
f"at path {path + _METADATA_FILE_SUFFIX}: "
|
|
653
|
+
f"metadata_size_bytes={len(metadata)}, "
|
|
654
|
+
f"tmp_suffix={tmp}, error={e}",
|
|
655
|
+
exc_info=True,
|
|
656
|
+
)
|
|
657
|
+
with self.hot_lock:
|
|
658
|
+
self.hot_cache.pop(key, None)
|
|
659
|
+
return
|
|
660
|
+
finally:
|
|
661
|
+
memory_obj.ref_count_down()
|
|
662
|
+
with self.put_lock:
|
|
663
|
+
self.put_tasks.discard(key)
|
|
664
|
+
|
|
665
|
+
# Call the completion callback if provided
|
|
666
|
+
if on_complete_callback is not None:
|
|
667
|
+
try:
|
|
668
|
+
on_complete_callback(key)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
logger.error(
|
|
671
|
+
f"on_complete_callback failed for key {key.to_string()}: {e}",
|
|
672
|
+
exc_info=True,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _handle_metadata_write_completion(
|
|
676
|
+
self, task: asyncio.Task, key: CacheEngineKey, path: str
|
|
677
|
+
) -> None:
|
|
678
|
+
"""Handle completion of metadata write task, checking for exceptions."""
|
|
679
|
+
try:
|
|
680
|
+
# Retrieve exception if task failed
|
|
681
|
+
exception = task.exception()
|
|
682
|
+
if exception is not None:
|
|
683
|
+
logger.error(
|
|
684
|
+
f"Metadata write task failed for key {key.to_string()} "
|
|
685
|
+
f"at path {path + _METADATA_FILE_SUFFIX}: {exception}",
|
|
686
|
+
exc_info=exception,
|
|
687
|
+
)
|
|
688
|
+
with self.hot_lock:
|
|
689
|
+
self.hot_cache.pop(key, None)
|
|
690
|
+
except Exception as e:
|
|
691
|
+
# Exception calling task.exception() (e.g., task was cancelled)
|
|
692
|
+
logger.error(
|
|
693
|
+
f"Error checking metadata write task status for key "
|
|
694
|
+
f"{key.to_string()}: {e}",
|
|
695
|
+
exc_info=True,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
def insert_key(self, key: CacheEngineKey, memory_obj: MemoryObj) -> None:
|
|
699
|
+
path, _, _, _ = self._key_to_path(key)
|
|
700
|
+
size = memory_obj.get_physical_size()
|
|
701
|
+
shape = memory_obj.metadata.shape
|
|
702
|
+
dtype = memory_obj.metadata.dtype
|
|
703
|
+
fmt = memory_obj.metadata.fmt
|
|
704
|
+
with self.hot_lock:
|
|
705
|
+
# TODO(Jiayi): need to support `cached_positions`.
|
|
706
|
+
self.hot_cache[key] = DiskCacheMetadata(path, size, shape, dtype, None, fmt)
|
|
707
|
+
|
|
708
|
+
def submit_prefetch_task(
|
|
709
|
+
self,
|
|
710
|
+
key: CacheEngineKey,
|
|
711
|
+
) -> bool:
|
|
712
|
+
# with self.hot_lock:
|
|
713
|
+
# entry = self.hot_cache.get(key)
|
|
714
|
+
# if entry is None:
|
|
715
|
+
# return None
|
|
716
|
+
|
|
717
|
+
# path = entry.path
|
|
718
|
+
# dtype = entry.dtype
|
|
719
|
+
# shape = entry.shape
|
|
720
|
+
# fmt = entry.fmt
|
|
721
|
+
# assert dtype is not None
|
|
722
|
+
# assert shape is not None
|
|
723
|
+
# assert fmt is not None
|
|
724
|
+
# return asyncio.run_coroutine_threadsafe(
|
|
725
|
+
# self._async_load_bytes_from_disk(key, path, dtype, shape,fmt), self.loop
|
|
726
|
+
# )
|
|
727
|
+
|
|
728
|
+
# TODO(Jiayi): Need to modify this when prefetch interface is determined.
|
|
729
|
+
|
|
730
|
+
# TODO(Jiayi): add `test_gds_backend_sanity` back after implementing this
|
|
731
|
+
return False
|
|
732
|
+
|
|
733
|
+
async def _async_load_bytes_from_disk(
|
|
734
|
+
self,
|
|
735
|
+
key: CacheEngineKey,
|
|
736
|
+
path: str,
|
|
737
|
+
dtype: torch.dtype,
|
|
738
|
+
shape: torch.Size,
|
|
739
|
+
fmt: MemoryFormat,
|
|
740
|
+
) -> Optional[MemoryObj]:
|
|
741
|
+
return self._load_bytes_from_disk_with_allocation(
|
|
742
|
+
key, path, dtype, shape, fmt=fmt
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def get_blocking(
|
|
746
|
+
self,
|
|
747
|
+
key: CacheEngineKey,
|
|
748
|
+
) -> Optional[MemoryObj]:
|
|
749
|
+
with self.hot_lock:
|
|
750
|
+
entry = self.hot_cache.get(key)
|
|
751
|
+
if entry is None:
|
|
752
|
+
return None
|
|
753
|
+
|
|
754
|
+
path = entry.path
|
|
755
|
+
dtype = entry.dtype
|
|
756
|
+
shape = entry.shape
|
|
757
|
+
fmt = entry.fmt
|
|
758
|
+
logger.warning(entry)
|
|
759
|
+
assert dtype is not None
|
|
760
|
+
assert shape is not None
|
|
761
|
+
assert fmt is not None
|
|
762
|
+
return self._load_bytes_from_disk_with_allocation(
|
|
763
|
+
key, path, dtype=dtype, shape=shape, fmt=fmt
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
def _load_bytes_from_disk_with_allocation(
|
|
767
|
+
self,
|
|
768
|
+
key: CacheEngineKey,
|
|
769
|
+
path: str,
|
|
770
|
+
dtype: torch.dtype,
|
|
771
|
+
shape: torch.Size,
|
|
772
|
+
fmt: MemoryFormat,
|
|
773
|
+
) -> Optional[MemoryObj]:
|
|
774
|
+
"""
|
|
775
|
+
Load byte array from disk by first allocating memory, then loading.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
key: Cache key for error handling
|
|
779
|
+
path: File path to load from
|
|
780
|
+
dtype: Data type for memory allocation
|
|
781
|
+
shape: Shape for memory allocation
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
A new memory object with loaded data, or None if allocation or
|
|
785
|
+
loading failed
|
|
786
|
+
"""
|
|
787
|
+
memory_obj = self.memory_allocator.allocate(shape, dtype, fmt=fmt)
|
|
788
|
+
if memory_obj is None:
|
|
789
|
+
logger.error("Memory allocation failed during sync disk load.")
|
|
790
|
+
return None
|
|
791
|
+
if self._debug_asserts:
|
|
792
|
+
assert memory_obj.tensor is not None
|
|
793
|
+
assert memory_obj.tensor.is_cuda
|
|
794
|
+
assert torch.device(self.dst_device) == torch.device(
|
|
795
|
+
memory_obj.tensor.device
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
return self._load_bytes_from_disk_with_memory(key, path, memory_obj)
|
|
799
|
+
|
|
800
|
+
def _load_bytes_from_disk_with_memory(
|
|
801
|
+
self,
|
|
802
|
+
key: CacheEngineKey,
|
|
803
|
+
path: str,
|
|
804
|
+
memory_obj: Optional[MemoryObj],
|
|
805
|
+
) -> Optional[MemoryObj]:
|
|
806
|
+
"""
|
|
807
|
+
Load byte array from disk into a pre-allocated memory object.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
key: Cache key for error handling
|
|
811
|
+
path: File path to load from
|
|
812
|
+
memory_obj: Pre-allocated memory object to load data into
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
The memory object with loaded data, or None if loading failed
|
|
816
|
+
"""
|
|
817
|
+
if memory_obj is None or not memory_obj.is_valid():
|
|
818
|
+
return None
|
|
819
|
+
|
|
820
|
+
offset = _METADATA_MAX_SIZE
|
|
821
|
+
if self.gds_base_pointer is None:
|
|
822
|
+
tensor = memory_obj.tensor
|
|
823
|
+
assert tensor is not None
|
|
824
|
+
if self._debug_asserts:
|
|
825
|
+
assert tensor.is_cuda
|
|
826
|
+
assert torch.device(self.dst_device) == torch.device(tensor.device)
|
|
827
|
+
addr = ctypes.c_void_p(tensor.data_ptr())
|
|
828
|
+
dev_offset = 0
|
|
829
|
+
else:
|
|
830
|
+
addr = ctypes.c_void_p(self.gds_base_pointer)
|
|
831
|
+
dev_offset = memory_obj.metadata.address
|
|
832
|
+
ret = self._load_gds(path, offset, addr, memory_obj.get_size(), dev_offset)
|
|
833
|
+
if ret != memory_obj.get_size():
|
|
834
|
+
if ret < 0:
|
|
835
|
+
logger.error(
|
|
836
|
+
f"Error loading {path}: ret: {ret} removing entry from cache"
|
|
837
|
+
)
|
|
838
|
+
with self.hot_lock:
|
|
839
|
+
self.hot_cache.pop(key)
|
|
840
|
+
else:
|
|
841
|
+
# TODO: we should probably count errors and
|
|
842
|
+
# remove the entry if it's a persistent problem.
|
|
843
|
+
logger.error(
|
|
844
|
+
f"Error loading {path}: got only {ret} bytes "
|
|
845
|
+
f"out of {memory_obj.get_size()}, ignoring"
|
|
846
|
+
)
|
|
847
|
+
memory_obj.ref_count_down()
|
|
848
|
+
return None
|
|
849
|
+
return memory_obj
|
|
850
|
+
|
|
851
|
+
def get_non_blocking(
|
|
852
|
+
self,
|
|
853
|
+
key: CacheEngineKey,
|
|
854
|
+
location: Optional[str] = None,
|
|
855
|
+
) -> Optional[Future]:
|
|
856
|
+
# TODO: Using a dummy wrapper around prefetch for now.
|
|
857
|
+
if not self.submit_prefetch_task(key):
|
|
858
|
+
return None
|
|
859
|
+
return Future()
|
|
860
|
+
|
|
861
|
+
def batched_get_blocking(
|
|
862
|
+
self,
|
|
863
|
+
keys: List[CacheEngineKey],
|
|
864
|
+
) -> List[Optional[MemoryObj]]:
|
|
865
|
+
if self.use_thread_pool:
|
|
866
|
+
logger.debug("Using batched_get_blocking with thread pool implementation")
|
|
867
|
+
return self._batched_get_blocking_by_thread_pool_impl(keys)
|
|
868
|
+
else:
|
|
869
|
+
return super().batched_get_blocking(keys)
|
|
870
|
+
|
|
871
|
+
def _batched_get_blocking_by_thread_pool_impl(
|
|
872
|
+
self,
|
|
873
|
+
keys: List[CacheEngineKey],
|
|
874
|
+
) -> list[MemoryObj | None]:
|
|
875
|
+
paths: list[str | None] = []
|
|
876
|
+
dtypes: list[torch.dtype | None] = []
|
|
877
|
+
shapes: list[torch.Size | None] = []
|
|
878
|
+
fmts: list[MemoryFormat | None] = []
|
|
879
|
+
with self.hot_lock:
|
|
880
|
+
for key in keys:
|
|
881
|
+
entry = self.hot_cache.get(key)
|
|
882
|
+
if entry is None:
|
|
883
|
+
logger.error(f"Lookup failed during get_blocking for {key}")
|
|
884
|
+
paths.append(None)
|
|
885
|
+
dtypes.append(None)
|
|
886
|
+
shapes.append(None)
|
|
887
|
+
fmts.append(None)
|
|
888
|
+
continue
|
|
889
|
+
paths.append(entry.path)
|
|
890
|
+
dtypes.append(entry.dtype)
|
|
891
|
+
shapes.append(entry.shape)
|
|
892
|
+
fmts.append(entry.fmt)
|
|
893
|
+
|
|
894
|
+
memory_objs: list[MemoryObj | None] = []
|
|
895
|
+
gds_reads, gds_read_bytes = 0, 0
|
|
896
|
+
for dtype, shape, path, fmt in zip(dtypes, shapes, paths, fmts, strict=True):
|
|
897
|
+
if path is None:
|
|
898
|
+
memory_objs.append(None)
|
|
899
|
+
continue
|
|
900
|
+
memory_obj = self.memory_allocator.allocate(shape, dtype, fmt=fmt)
|
|
901
|
+
if memory_obj is None:
|
|
902
|
+
logger.error(f"Memory allocation failed during get_blocking for {path}")
|
|
903
|
+
else:
|
|
904
|
+
gds_reads += 1
|
|
905
|
+
gds_read_bytes += memory_obj.get_size()
|
|
906
|
+
memory_objs.append(memory_obj)
|
|
907
|
+
|
|
908
|
+
start_time = time.perf_counter()
|
|
909
|
+
assert self._thread_pool is not None
|
|
910
|
+
results = list(
|
|
911
|
+
self._thread_pool.map(
|
|
912
|
+
self._load_bytes_from_disk_with_memory, keys, paths, memory_objs
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
total_time = time.perf_counter() - start_time
|
|
916
|
+
logger.info(
|
|
917
|
+
f"Time taken for batched_get_blocking: {total_time:.3f}s |"
|
|
918
|
+
f" {gds_read_bytes / 1024 / 1024}MiB | {gds_reads} ops."
|
|
919
|
+
)
|
|
920
|
+
return results
|
|
921
|
+
|
|
922
|
+
@_lmcache_nvtx_annotate
|
|
923
|
+
@torch.inference_mode()
|
|
924
|
+
def _save_gds(
|
|
925
|
+
self,
|
|
926
|
+
path: str,
|
|
927
|
+
tmp: str,
|
|
928
|
+
kv_chunk: torch.Tensor,
|
|
929
|
+
fmt: MemoryFormat,
|
|
930
|
+
base_pointer: int,
|
|
931
|
+
device_offset: int,
|
|
932
|
+
):
|
|
933
|
+
if base_pointer is None:
|
|
934
|
+
addr = ctypes.c_void_p(kv_chunk.data_ptr())
|
|
935
|
+
dev_offset = 0
|
|
936
|
+
else:
|
|
937
|
+
addr = ctypes.c_void_p(base_pointer)
|
|
938
|
+
dev_offset = device_offset
|
|
939
|
+
tmp_path = path + tmp
|
|
940
|
+
offset = _METADATA_MAX_SIZE
|
|
941
|
+
# TODO: We can add the chunk's metadata here, e.g. Tensor parallelism shard
|
|
942
|
+
# and pipeline parallelism index.
|
|
943
|
+
metadata = pack_metadata(
|
|
944
|
+
kv_chunk, fmt=fmt, lmcache_version=str(_METADATA_VERSION)
|
|
945
|
+
)
|
|
946
|
+
try:
|
|
947
|
+
with open(tmp_path, "wb") as f:
|
|
948
|
+
f.write(metadata)
|
|
949
|
+
if self.gds_module:
|
|
950
|
+
with self.gds_module.CuFile(
|
|
951
|
+
tmp_path, "r+", use_direct_io=self.use_direct_io
|
|
952
|
+
) as f:
|
|
953
|
+
f.write(
|
|
954
|
+
addr, kv_chunk.nbytes, file_offset=offset, dev_offset=dev_offset
|
|
955
|
+
)
|
|
956
|
+
elif self.cudart:
|
|
957
|
+
# mmap the file
|
|
958
|
+
fd = os.open(tmp_path, os.O_RDWR)
|
|
959
|
+
nbytes = kv_chunk.nbytes
|
|
960
|
+
os.ftruncate(fd, nbytes + offset)
|
|
961
|
+
mm = mmap.mmap(
|
|
962
|
+
fd, nbytes + offset, prot=mmap.PROT_WRITE, flags=mmap.MAP_SHARED
|
|
963
|
+
)
|
|
964
|
+
os.close(fd)
|
|
965
|
+
|
|
966
|
+
# get mapped file address
|
|
967
|
+
arr = np.frombuffer(mm, dtype=np.uint8)
|
|
968
|
+
buf_addr = arr.__array_interface__["data"][0]
|
|
969
|
+
|
|
970
|
+
assert addr.value is not None
|
|
971
|
+
res = self.cudart.cudaMemcpy(
|
|
972
|
+
ctypes.c_void_p(buf_addr + offset),
|
|
973
|
+
ctypes.c_void_p(int(addr.value) + device_offset),
|
|
974
|
+
ctypes.c_size_t(nbytes),
|
|
975
|
+
ctypes.c_int(2),
|
|
976
|
+
)
|
|
977
|
+
if res:
|
|
978
|
+
raise RuntimeError(f"cudaMemcpy failed {res}")
|
|
979
|
+
del arr
|
|
980
|
+
mm.close()
|
|
981
|
+
|
|
982
|
+
except Exception as e:
|
|
983
|
+
logger.error(f"Error saving {tmp_path}: {e}", exc_info=True)
|
|
984
|
+
raise e
|
|
985
|
+
os.rename(tmp_path, path)
|
|
986
|
+
return metadata
|
|
987
|
+
|
|
988
|
+
def _load_gds(
|
|
989
|
+
self,
|
|
990
|
+
gds_path: str,
|
|
991
|
+
file_offset: int,
|
|
992
|
+
gpu_pointer: ctypes.c_void_p,
|
|
993
|
+
size_in_bytes: int,
|
|
994
|
+
dev_offset: int,
|
|
995
|
+
) -> int:
|
|
996
|
+
"""Read data from disk into a GPU buffer"""
|
|
997
|
+
try:
|
|
998
|
+
if self.gds_module:
|
|
999
|
+
with self.gds_module.CuFile(
|
|
1000
|
+
gds_path, "r", use_direct_io=self.use_direct_io
|
|
1001
|
+
) as f:
|
|
1002
|
+
return f.read(
|
|
1003
|
+
gpu_pointer,
|
|
1004
|
+
size_in_bytes,
|
|
1005
|
+
file_offset=file_offset,
|
|
1006
|
+
dev_offset=dev_offset,
|
|
1007
|
+
)
|
|
1008
|
+
elif self.cudart:
|
|
1009
|
+
fd = os.open(gds_path, os.O_RDONLY)
|
|
1010
|
+
file_size = os.fstat(fd).st_size
|
|
1011
|
+
|
|
1012
|
+
# Check if file is large enough for the requested read
|
|
1013
|
+
if file_size < file_offset + size_in_bytes:
|
|
1014
|
+
os.close(fd)
|
|
1015
|
+
logger.error(
|
|
1016
|
+
f"File {gds_path} is too small: size={file_size}, "
|
|
1017
|
+
f"but need at least {file_offset + size_in_bytes} bytes "
|
|
1018
|
+
f"(offset={file_offset}, requested={size_in_bytes})"
|
|
1019
|
+
)
|
|
1020
|
+
return -1
|
|
1021
|
+
|
|
1022
|
+
mm = mmap.mmap(
|
|
1023
|
+
fd,
|
|
1024
|
+
file_size,
|
|
1025
|
+
prot=mmap.PROT_READ,
|
|
1026
|
+
flags=mmap.MAP_PRIVATE | mmap.MAP_POPULATE, # type: ignore [attr-defined]
|
|
1027
|
+
)
|
|
1028
|
+
os.close(fd)
|
|
1029
|
+
|
|
1030
|
+
arr = np.frombuffer(mm, dtype=np.uint8)
|
|
1031
|
+
addr = arr.__array_interface__["data"][0]
|
|
1032
|
+
|
|
1033
|
+
assert gpu_pointer.value is not None
|
|
1034
|
+
res = self.cudart.cudaMemcpy(
|
|
1035
|
+
ctypes.c_void_p(int(gpu_pointer.value) + dev_offset),
|
|
1036
|
+
ctypes.c_void_p(addr + file_offset),
|
|
1037
|
+
ctypes.c_size_t(size_in_bytes),
|
|
1038
|
+
ctypes.c_int(1),
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
if res != 0:
|
|
1042
|
+
raise RuntimeError(f"cudaMemcpy failed with code {res}")
|
|
1043
|
+
del arr
|
|
1044
|
+
mm.close()
|
|
1045
|
+
return size_in_bytes
|
|
1046
|
+
else:
|
|
1047
|
+
raise RuntimeError(
|
|
1048
|
+
"Both gds_module and cudart are None, this should not happen"
|
|
1049
|
+
)
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
# return -1 on any exception, and log the error.
|
|
1052
|
+
# The caller will handle the error by removing the cache entry and
|
|
1053
|
+
# returning None.
|
|
1054
|
+
logger.error(f"GDS read failed for {gds_path}: {e}", exc_info=True)
|
|
1055
|
+
return -1
|
|
1056
|
+
|
|
1057
|
+
def pin(self, key: CacheEngineKey) -> bool:
|
|
1058
|
+
# NOTE (ApostaC): Since gds doesn't have eviction now, we don't need
|
|
1059
|
+
# to implement pin and unpin
|
|
1060
|
+
return False
|
|
1061
|
+
|
|
1062
|
+
def unpin(self, key: CacheEngineKey) -> bool:
|
|
1063
|
+
# NOTE (ApostaC): Since gds doesn't have eviction now, we don't need
|
|
1064
|
+
# to implement pin and unpin
|
|
1065
|
+
return False
|
|
1066
|
+
|
|
1067
|
+
def remove(self, key: CacheEngineKey, force: bool = True):
|
|
1068
|
+
raise NotImplementedError("Remote backend does not support remove now.")
|
|
1069
|
+
|
|
1070
|
+
def initialize_allocator(
|
|
1071
|
+
self, config: LMCacheEngineConfig, metadata: LMCacheMetadata
|
|
1072
|
+
) -> Union[CuFileMemoryAllocator, HipFileMemoryAllocator]:
|
|
1073
|
+
assert config.gds_buffer_size is not None
|
|
1074
|
+
allocator_cls = (
|
|
1075
|
+
HipFileMemoryAllocator
|
|
1076
|
+
if self.gds_backend == "hipfile"
|
|
1077
|
+
else CuFileMemoryAllocator
|
|
1078
|
+
)
|
|
1079
|
+
return allocator_cls(config.gds_buffer_size * 1024**2)
|
|
1080
|
+
|
|
1081
|
+
def allocate(
|
|
1082
|
+
self,
|
|
1083
|
+
shapes: Union[torch.Size, list[torch.Size]],
|
|
1084
|
+
dtypes: Union[torch.dtype, list[torch.dtype]],
|
|
1085
|
+
fmt: MemoryFormat = MemoryFormat.KV_2LTD,
|
|
1086
|
+
eviction: bool = True,
|
|
1087
|
+
busy_loop: bool = True,
|
|
1088
|
+
) -> Optional[MemoryObj]:
|
|
1089
|
+
"""
|
|
1090
|
+
Allocate a memory object of shape and dtype
|
|
1091
|
+
"""
|
|
1092
|
+
if eviction:
|
|
1093
|
+
logger.warning("GDS Backend does not support eviction")
|
|
1094
|
+
|
|
1095
|
+
logger.debug(f"Allocating memory with busy loop: {busy_loop}")
|
|
1096
|
+
|
|
1097
|
+
max_attempts = self.max_alloc_attempts if busy_loop else 1
|
|
1098
|
+
num_attempts = 0
|
|
1099
|
+
|
|
1100
|
+
# try up to max_attempts
|
|
1101
|
+
while True:
|
|
1102
|
+
memory_obj = self.memory_allocator.allocate(shapes, dtypes, fmt)
|
|
1103
|
+
if memory_obj is not None: # success
|
|
1104
|
+
return memory_obj
|
|
1105
|
+
|
|
1106
|
+
num_attempts += 1
|
|
1107
|
+
if num_attempts < max_attempts: # keep trying until max attempts is reached
|
|
1108
|
+
logger.debug(
|
|
1109
|
+
f"Unable to allocate memory object after {num_attempts} "
|
|
1110
|
+
f"attempt(s) of GDS backend allocate(). "
|
|
1111
|
+
f"Waiting {self.alloc_attempt_delay_secs} seconds before retrying."
|
|
1112
|
+
)
|
|
1113
|
+
if self.alloc_attempt_delay_secs > 0:
|
|
1114
|
+
time.sleep(self.alloc_attempt_delay_secs)
|
|
1115
|
+
else: # break to failure case after max attempts is reached
|
|
1116
|
+
break
|
|
1117
|
+
|
|
1118
|
+
logger.warning(
|
|
1119
|
+
f"GDS allocation failed after {num_attempts} attempt(s). Returning None."
|
|
1120
|
+
)
|
|
1121
|
+
if not self.memory_allocator.memcheck():
|
|
1122
|
+
logger.error(
|
|
1123
|
+
"GDS allocation failed and memory allocator "
|
|
1124
|
+
"is inconsistent. This is a bug in the memory allocator."
|
|
1125
|
+
)
|
|
1126
|
+
return None
|
|
1127
|
+
|
|
1128
|
+
def batched_allocate(
|
|
1129
|
+
self,
|
|
1130
|
+
shapes: Union[torch.Size, list[torch.Size]],
|
|
1131
|
+
dtypes: Union[torch.dtype, list[torch.dtype]],
|
|
1132
|
+
batch_size: int,
|
|
1133
|
+
fmt: MemoryFormat = MemoryFormat.KV_2LTD,
|
|
1134
|
+
eviction: bool = True,
|
|
1135
|
+
busy_loop: bool = True,
|
|
1136
|
+
) -> Optional[list[MemoryObj]]:
|
|
1137
|
+
"""
|
|
1138
|
+
Batched allocate `batch_size` memory objects of shape and dtype
|
|
1139
|
+
"""
|
|
1140
|
+
if eviction:
|
|
1141
|
+
logger.warning("GDS Backend does not support eviction")
|
|
1142
|
+
|
|
1143
|
+
logger.debug(
|
|
1144
|
+
f"Batched allocating memory in GDS backend with busy loop: {busy_loop}"
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
max_attempts = self.max_alloc_attempts if busy_loop else 1
|
|
1148
|
+
num_attempts = 0
|
|
1149
|
+
|
|
1150
|
+
# try up to max_attempts
|
|
1151
|
+
while True:
|
|
1152
|
+
memory_objs = self.memory_allocator.batched_allocate(
|
|
1153
|
+
shapes, dtypes, batch_size, fmt
|
|
1154
|
+
)
|
|
1155
|
+
if memory_objs is not None: # success
|
|
1156
|
+
return memory_objs
|
|
1157
|
+
|
|
1158
|
+
num_attempts += 1
|
|
1159
|
+
if num_attempts < max_attempts: # keep trying until max attempts is reached
|
|
1160
|
+
logger.debug(
|
|
1161
|
+
f"Unable to allocate memory object after {num_attempts} "
|
|
1162
|
+
f"attempt(s) of GDS backend batched_allocate(). "
|
|
1163
|
+
f"Waiting {self.alloc_attempt_delay_secs} seconds before retrying."
|
|
1164
|
+
)
|
|
1165
|
+
if self.alloc_attempt_delay_secs > 0:
|
|
1166
|
+
time.sleep(self.alloc_attempt_delay_secs)
|
|
1167
|
+
else: # break to failure case after max attempts is reached
|
|
1168
|
+
break
|
|
1169
|
+
|
|
1170
|
+
logger.warning(
|
|
1171
|
+
f"GDS batched allocation failed after {num_attempts} "
|
|
1172
|
+
f"attempt(s). Returning None."
|
|
1173
|
+
)
|
|
1174
|
+
if not self.memory_allocator.memcheck():
|
|
1175
|
+
logger.error(
|
|
1176
|
+
"GDS batched allocation failed and memory allocator "
|
|
1177
|
+
"is inconsistent. This is a bug in the memory allocator."
|
|
1178
|
+
)
|
|
1179
|
+
return None
|
|
1180
|
+
|
|
1181
|
+
def get_allocator_backend(self):
|
|
1182
|
+
return self
|
|
1183
|
+
|
|
1184
|
+
def get_memory_allocator(self):
|
|
1185
|
+
return self.memory_allocator
|
|
1186
|
+
|
|
1187
|
+
def close(self) -> None:
|
|
1188
|
+
# Wait for initial metadata scan to complete
|
|
1189
|
+
try:
|
|
1190
|
+
self._scan_metadata_future.result(timeout=30)
|
|
1191
|
+
except Exception as e:
|
|
1192
|
+
logger.warning(
|
|
1193
|
+
f"Exception while waiting for metadata scan: {e}",
|
|
1194
|
+
exc_info=True,
|
|
1195
|
+
)
|
|
1196
|
+
self.memory_allocator.close()
|
|
1197
|
+
if self._thread_pool is not None:
|
|
1198
|
+
self._thread_pool.shutdown(wait=True)
|
|
1199
|
+
logger.info("GDS backend closed.")
|