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,699 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Standard
|
|
3
|
+
from enum import IntEnum, auto
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from urllib.parse import quote as url_quote
|
|
6
|
+
import asyncio
|
|
7
|
+
import ctypes
|
|
8
|
+
|
|
9
|
+
# Third Party
|
|
10
|
+
from awscrt import auth, io, s3
|
|
11
|
+
from awscrt.http import HttpHeaders, HttpRequest
|
|
12
|
+
from awscrt.io import ClientTlsContext, TlsConnectionOptions, TlsContextOptions
|
|
13
|
+
|
|
14
|
+
# First Party
|
|
15
|
+
from lmcache.logging import init_logger
|
|
16
|
+
from lmcache.utils import CacheEngineKey
|
|
17
|
+
from lmcache.v1.memory_management import MemoryObj
|
|
18
|
+
from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
|
|
19
|
+
from lmcache.v1.storage_backend.job_executor.pq_executor import AsyncPQExecutor
|
|
20
|
+
from lmcache.v1.storage_backend.local_cpu_backend import LocalCPUBackend
|
|
21
|
+
|
|
22
|
+
logger = init_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Priorities(IntEnum):
|
|
26
|
+
PEEK = auto()
|
|
27
|
+
PREFETCH = auto()
|
|
28
|
+
GET = auto()
|
|
29
|
+
PUT = auto()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# zero copy helper for S3 upload
|
|
33
|
+
class MemoryViewStream:
|
|
34
|
+
def __init__(self, mv: bytes):
|
|
35
|
+
# casting does not copy
|
|
36
|
+
# we just get a uint8 view
|
|
37
|
+
self.mv = memoryview(mv).cast("B")
|
|
38
|
+
self.offset = 0
|
|
39
|
+
|
|
40
|
+
def read(self, size=None):
|
|
41
|
+
if size is None:
|
|
42
|
+
size = len(self.mv) - self.offset
|
|
43
|
+
if size < 0:
|
|
44
|
+
size = 0
|
|
45
|
+
|
|
46
|
+
end = min(self.offset + size, len(self.mv))
|
|
47
|
+
result = self.mv[self.offset : end]
|
|
48
|
+
self.offset = end
|
|
49
|
+
# CRT/Python accepts memoryview
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
def seek(self, offset, whence=0):
|
|
53
|
+
if whence == 0:
|
|
54
|
+
self.offset = offset
|
|
55
|
+
elif whence == 1:
|
|
56
|
+
self.offset += offset
|
|
57
|
+
elif whence == 2:
|
|
58
|
+
self.offset = len(self.mv) + offset
|
|
59
|
+
return self.offset
|
|
60
|
+
|
|
61
|
+
def tell(self):
|
|
62
|
+
return self.offset
|
|
63
|
+
|
|
64
|
+
def __len__(self):
|
|
65
|
+
return len(self.mv)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class S3Connector(RemoteConnector):
|
|
69
|
+
"""
|
|
70
|
+
S3 remote connector
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
s3_endpoint: str,
|
|
76
|
+
loop: asyncio.AbstractEventLoop,
|
|
77
|
+
local_cpu_backend: LocalCPUBackend,
|
|
78
|
+
s3_num_io_threads: int,
|
|
79
|
+
s3_prefer_http2: bool,
|
|
80
|
+
s3_region: str,
|
|
81
|
+
s3_enable_s3express: bool,
|
|
82
|
+
disable_tls: bool,
|
|
83
|
+
aws_access_key_id: Optional[str] = None,
|
|
84
|
+
aws_secret_access_key: Optional[str] = None,
|
|
85
|
+
):
|
|
86
|
+
# initialize base class, which includes some common attributes
|
|
87
|
+
super().__init__(local_cpu_backend.config, local_cpu_backend.metadata)
|
|
88
|
+
|
|
89
|
+
if not s3_endpoint.startswith("s3://"):
|
|
90
|
+
raise ValueError("S3 url must start with 's3://'")
|
|
91
|
+
|
|
92
|
+
self.s3_part_size = self.full_chunk_size_bytes
|
|
93
|
+
|
|
94
|
+
self.s3_endpoint = s3_endpoint.removeprefix("s3://")
|
|
95
|
+
self.loop = loop
|
|
96
|
+
self.local_cpu_backend = local_cpu_backend
|
|
97
|
+
|
|
98
|
+
self.s3_num_io_threads = s3_num_io_threads
|
|
99
|
+
self.s3_prefer_http2 = s3_prefer_http2
|
|
100
|
+
self.s3_region = s3_region
|
|
101
|
+
self.s3_enable_s3express = s3_enable_s3express
|
|
102
|
+
|
|
103
|
+
event_loop_group = io.EventLoopGroup(s3_num_io_threads)
|
|
104
|
+
host_resolver = io.DefaultHostResolver(event_loop_group)
|
|
105
|
+
client_bootstrap = io.ClientBootstrap(event_loop_group, host_resolver)
|
|
106
|
+
if aws_access_key_id and aws_secret_access_key:
|
|
107
|
+
logger.info("Using explicit AWS credentials passed to S3Connector")
|
|
108
|
+
self.credentials_provider = auth.AwsCredentialsProvider.new_static(
|
|
109
|
+
aws_access_key_id,
|
|
110
|
+
aws_secret_access_key,
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
logger.info(
|
|
114
|
+
"No credentials provider, trying to use credentials from environment"
|
|
115
|
+
)
|
|
116
|
+
self.credentials_provider = auth.AwsCredentialsProvider.new_default_chain(
|
|
117
|
+
client_bootstrap
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
tls_opts = None
|
|
121
|
+
|
|
122
|
+
if self.s3_prefer_http2:
|
|
123
|
+
# Use HTTP/2 multiplexing if possible.
|
|
124
|
+
tls_ctx = ClientTlsContext(TlsContextOptions())
|
|
125
|
+
tls_opts = TlsConnectionOptions(tls_ctx)
|
|
126
|
+
try:
|
|
127
|
+
tls_opts.set_alpn_list(["h2", "http/1.1"])
|
|
128
|
+
except Exception:
|
|
129
|
+
tls_opts = None
|
|
130
|
+
|
|
131
|
+
signing_config = None
|
|
132
|
+
if self.s3_enable_s3express:
|
|
133
|
+
signing_config = auth.AwsSigningConfig(
|
|
134
|
+
algorithm=auth.AwsSigningAlgorithm.V4_S3EXPRESS,
|
|
135
|
+
region=self.s3_region,
|
|
136
|
+
service="s3",
|
|
137
|
+
credentials_provider=self.credentials_provider,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# turn off TLS for non-AWS services
|
|
141
|
+
# regular and directory/express buckets both use TLS by default
|
|
142
|
+
turn_off_tls = (
|
|
143
|
+
s3.S3RequestTlsMode.DISABLED if disable_tls else s3.S3RequestTlsMode.ENABLED
|
|
144
|
+
)
|
|
145
|
+
logger.info("Initializing S3 client")
|
|
146
|
+
self.s3_client = s3.S3Client(
|
|
147
|
+
bootstrap=client_bootstrap,
|
|
148
|
+
region=s3_region,
|
|
149
|
+
enable_s3express=s3_enable_s3express,
|
|
150
|
+
tls_connection_options=tls_opts,
|
|
151
|
+
tls_mode=turn_off_tls,
|
|
152
|
+
signing_config=signing_config,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# TODO(Jiayi): We need to handle cache consistency issues in a systematic way
|
|
156
|
+
# across all connectors.
|
|
157
|
+
# We assume S3 cache is never evicted and read-only for now.
|
|
158
|
+
# the object size cache does not need protection because
|
|
159
|
+
# asyncio scheduling is cooperative and not preemptive
|
|
160
|
+
self.object_size_cache: dict[str, int] = {}
|
|
161
|
+
|
|
162
|
+
# Circuit breaker for connection failures
|
|
163
|
+
self.connection_failures = 0
|
|
164
|
+
self.max_connection_failures = 3
|
|
165
|
+
self.connection_disabled = False
|
|
166
|
+
|
|
167
|
+
self.pq_executor = AsyncPQExecutor(loop)
|
|
168
|
+
|
|
169
|
+
def _format_safe_path(self, key_str: str) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Generate a safe HTTP path for the S3 key.
|
|
172
|
+
Flattens the key by replacing slashes with underscores and URL-encodes
|
|
173
|
+
any special characters.
|
|
174
|
+
"""
|
|
175
|
+
flat_key_str = key_str.replace("/", "_")
|
|
176
|
+
return "/" + url_quote(flat_key_str)
|
|
177
|
+
|
|
178
|
+
# TODO(Jiayi): optimize this with async
|
|
179
|
+
def _get_object_size(self, key_str: str) -> int:
|
|
180
|
+
headers = HttpHeaders()
|
|
181
|
+
headers.add("Host", self.s3_endpoint)
|
|
182
|
+
req = HttpRequest("HEAD", self._format_safe_path(key_str), headers)
|
|
183
|
+
|
|
184
|
+
got = {"len": None, "status": None, "err": None}
|
|
185
|
+
|
|
186
|
+
def on_headers(status_code, headers, **kwargs):
|
|
187
|
+
got["status"] = status_code
|
|
188
|
+
for name, value in headers:
|
|
189
|
+
if name.lower() == "content-length":
|
|
190
|
+
try:
|
|
191
|
+
got["len"] = int(value)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
def on_done(error=None, **kwargs):
|
|
196
|
+
got["err"] = error
|
|
197
|
+
|
|
198
|
+
s3_req = s3.S3Request(
|
|
199
|
+
client=self.s3_client,
|
|
200
|
+
type=s3.S3RequestType.DEFAULT,
|
|
201
|
+
request=req,
|
|
202
|
+
operation_name="HeadObject",
|
|
203
|
+
on_headers=on_headers,
|
|
204
|
+
on_done=on_done,
|
|
205
|
+
credential_provider=self.credentials_provider,
|
|
206
|
+
region=self.s3_region,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
s3_req.finished_future.result()
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# 404 (not found) is expected when checking if object exists
|
|
213
|
+
if got["status"] == 404:
|
|
214
|
+
logger.debug(f"Object not found: {key_str}")
|
|
215
|
+
else:
|
|
216
|
+
logger.debug(f"Exception in `_get_object_size`: {e}")
|
|
217
|
+
return 0
|
|
218
|
+
if got["err"] or got["status"] != 200:
|
|
219
|
+
if got["status"] != 404: # Don't warn for 404, it's expected
|
|
220
|
+
logger.warning(
|
|
221
|
+
"Encountering error in S3 HEAD request "
|
|
222
|
+
f"with error code: {got['status']}"
|
|
223
|
+
)
|
|
224
|
+
return 0
|
|
225
|
+
return got["len"] if got["len"] is not None else 0
|
|
226
|
+
|
|
227
|
+
# exactly the same as _get_object_size just awaiting an asyncio.Future
|
|
228
|
+
# instead of a concurrent.futures.Future
|
|
229
|
+
async def _get_object_size_async(self, key_str: str) -> int:
|
|
230
|
+
headers = HttpHeaders()
|
|
231
|
+
headers.add("Host", self.s3_endpoint)
|
|
232
|
+
req = HttpRequest("HEAD", self._format_safe_path(key_str), headers)
|
|
233
|
+
|
|
234
|
+
got = {"len": None, "status": None, "err": None}
|
|
235
|
+
|
|
236
|
+
def on_headers(status_code, headers, **kwargs):
|
|
237
|
+
got["status"] = status_code
|
|
238
|
+
for name, value in headers:
|
|
239
|
+
if name.lower() == "content-length":
|
|
240
|
+
try:
|
|
241
|
+
got["len"] = int(value)
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
def on_done(error=None, **kwargs):
|
|
246
|
+
got["err"] = error
|
|
247
|
+
|
|
248
|
+
s3_req = s3.S3Request(
|
|
249
|
+
client=self.s3_client,
|
|
250
|
+
type=s3.S3RequestType.DEFAULT,
|
|
251
|
+
request=req,
|
|
252
|
+
operation_name="HeadObject",
|
|
253
|
+
on_headers=on_headers,
|
|
254
|
+
on_done=on_done,
|
|
255
|
+
credential_provider=self.credentials_provider,
|
|
256
|
+
region=self.s3_region,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
await asyncio.wrap_future(s3_req.finished_future)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
# 404 (not found) is expected when checking if object exists
|
|
263
|
+
if got["status"] == 404:
|
|
264
|
+
logger.debug(f"Object not found: {key_str}")
|
|
265
|
+
else:
|
|
266
|
+
logger.debug(f"Exception in `_get_object_size_async`: {e}")
|
|
267
|
+
return 0
|
|
268
|
+
if got["err"] or got["status"] != 200:
|
|
269
|
+
if got["status"] != 404: # Don't warn for 404, it's expected
|
|
270
|
+
logger.warning(
|
|
271
|
+
"Encountering error in S3 HEAD request "
|
|
272
|
+
f"with error code: {got['status']}"
|
|
273
|
+
)
|
|
274
|
+
return 0
|
|
275
|
+
return got["len"] if got["len"] is not None else 0
|
|
276
|
+
|
|
277
|
+
async def exists(self, key: CacheEngineKey) -> bool:
|
|
278
|
+
return self.exists_sync(key)
|
|
279
|
+
|
|
280
|
+
def exists_sync(self, key: CacheEngineKey) -> bool:
|
|
281
|
+
# Circuit breaker: if connection is disabled, return False
|
|
282
|
+
if self.connection_disabled:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
key_str = key.to_string()
|
|
286
|
+
if key_str in self.object_size_cache:
|
|
287
|
+
return self.object_size_cache[key_str] > 0
|
|
288
|
+
cache_size = self._get_object_size(key_str)
|
|
289
|
+
if cache_size > 0:
|
|
290
|
+
self.object_size_cache[key_str] = cache_size
|
|
291
|
+
return True
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def _write_mem_obj(self, mem_obj: MemoryObj, data: bytes, offset: int):
|
|
295
|
+
ctypes.memmove(mem_obj.data_ptr + offset, data, len(data))
|
|
296
|
+
|
|
297
|
+
def _s3_download(
|
|
298
|
+
self,
|
|
299
|
+
key_str: str,
|
|
300
|
+
mem_obj: MemoryObj,
|
|
301
|
+
) -> "s3.S3Request":
|
|
302
|
+
"""
|
|
303
|
+
Download a file from S3.
|
|
304
|
+
"""
|
|
305
|
+
headers = HttpHeaders()
|
|
306
|
+
headers.add("Host", self.s3_endpoint)
|
|
307
|
+
|
|
308
|
+
# TODO(Jiayi): Enable more finegrained data partition
|
|
309
|
+
# range_header = f"bytes={start_byte}-{end_byte}"
|
|
310
|
+
# headers.add("Range", range_header)
|
|
311
|
+
|
|
312
|
+
req = HttpRequest("GET", self._format_safe_path(key_str), headers)
|
|
313
|
+
|
|
314
|
+
def on_body(chunk, offset, **kwargs):
|
|
315
|
+
# Directly write chunk to the memory object at the correct offset
|
|
316
|
+
self._write_mem_obj(mem_obj, chunk, offset)
|
|
317
|
+
|
|
318
|
+
# NOTE(Jiayi): Run in crt threads (not this thread) with GIL
|
|
319
|
+
# See https://github.com/awslabs/aws-crt-python/blob/4250709624119de1af3ca86816e1a154fcac7cc8/source/common.c#L51
|
|
320
|
+
def on_done(error=None, status_code=None, **kwargs):
|
|
321
|
+
ok = (status_code in (200, 206)) or (status_code is None)
|
|
322
|
+
if error or not ok:
|
|
323
|
+
raise RuntimeError(
|
|
324
|
+
f"Failed to download {key_str} from S3: {error or status_code}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# TODO(Jiayi): Need to support offset to enable zero-copy
|
|
328
|
+
# More concretely, we need to get the shared memory offset.
|
|
329
|
+
s3_req = s3.S3Request(
|
|
330
|
+
client=self.s3_client,
|
|
331
|
+
type=s3.S3RequestType.GET_OBJECT,
|
|
332
|
+
request=req,
|
|
333
|
+
on_body=on_body,
|
|
334
|
+
credential_provider=self.credentials_provider,
|
|
335
|
+
region=self.s3_region,
|
|
336
|
+
on_done=on_done,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return s3_req
|
|
340
|
+
|
|
341
|
+
async def get(self, key: CacheEngineKey) -> Optional[MemoryObj]:
|
|
342
|
+
# Circuit breaker: if connection is disabled, return None immediately
|
|
343
|
+
if self.connection_disabled:
|
|
344
|
+
logger.debug(
|
|
345
|
+
f"S3 connection disabled. Skipping download for {key.to_string()}"
|
|
346
|
+
)
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
key_str = key.to_string()
|
|
350
|
+
|
|
351
|
+
obj_size = self.object_size_cache.get(key_str, None)
|
|
352
|
+
|
|
353
|
+
if obj_size is None:
|
|
354
|
+
obj_size = await self._get_object_size_async(key_str)
|
|
355
|
+
if obj_size <= 0:
|
|
356
|
+
self.object_size_cache[key_str] = 0
|
|
357
|
+
return None
|
|
358
|
+
self.object_size_cache[key_str] = obj_size
|
|
359
|
+
|
|
360
|
+
memory_obj = self.local_cpu_backend.allocate(
|
|
361
|
+
self.meta_shapes,
|
|
362
|
+
self.meta_dtypes,
|
|
363
|
+
self.meta_fmt,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if memory_obj is None:
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
# Check if stored size matches expected size
|
|
370
|
+
if obj_size != memory_obj.get_size():
|
|
371
|
+
logger.error(
|
|
372
|
+
f"Size mismatch for {key_str}: S3 has {obj_size} bytes, "
|
|
373
|
+
f"but current config expects {memory_obj.get_size()} bytes. "
|
|
374
|
+
f"This usually means the data was stored with different chunk_size "
|
|
375
|
+
f"or model configuration. Please use matching config or clear S3."
|
|
376
|
+
)
|
|
377
|
+
memory_obj.ref_count_down()
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
s3_req = self._s3_download(
|
|
381
|
+
key_str=key_str,
|
|
382
|
+
mem_obj=memory_obj,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
# use blocking_timeout_sec in config to control the timeout
|
|
387
|
+
await asyncio.wrap_future(s3_req.finished_future)
|
|
388
|
+
|
|
389
|
+
# Reset failure counter on success
|
|
390
|
+
self._reset_connection_failures()
|
|
391
|
+
|
|
392
|
+
return memory_obj
|
|
393
|
+
except Exception as e:
|
|
394
|
+
error_msg = str(e)
|
|
395
|
+
|
|
396
|
+
# Update connection failures and check if it's a connection error
|
|
397
|
+
is_connection_error = self._update_connection_failures(error_msg)
|
|
398
|
+
|
|
399
|
+
if not is_connection_error:
|
|
400
|
+
# Log non-connection errors
|
|
401
|
+
logger.error(f"Failed to download {key_str} from S3: {e}")
|
|
402
|
+
|
|
403
|
+
memory_obj.ref_count_down()
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
async def batched_get(
|
|
407
|
+
self, keys: List[CacheEngineKey]
|
|
408
|
+
) -> List[Optional[MemoryObj]]:
|
|
409
|
+
# Circuit breaker: if connection is disabled, return all None
|
|
410
|
+
if self.connection_disabled:
|
|
411
|
+
logger.debug(
|
|
412
|
+
f"S3 connection disabled. "
|
|
413
|
+
f"Skipping batched download for {len(keys)} keys"
|
|
414
|
+
)
|
|
415
|
+
return [None] * len(keys)
|
|
416
|
+
|
|
417
|
+
memory_objs: List[Optional[MemoryObj]] = []
|
|
418
|
+
futures = []
|
|
419
|
+
future_to_memobj_idx = []
|
|
420
|
+
|
|
421
|
+
for idx, key in enumerate(keys):
|
|
422
|
+
key_str = key.to_string()
|
|
423
|
+
|
|
424
|
+
obj_size = self.object_size_cache.get(key_str, None)
|
|
425
|
+
|
|
426
|
+
if obj_size is None:
|
|
427
|
+
obj_size = await self._get_object_size_async(key_str)
|
|
428
|
+
if obj_size <= 0:
|
|
429
|
+
self.object_size_cache[key_str] = 0
|
|
430
|
+
memory_objs.append(None)
|
|
431
|
+
continue
|
|
432
|
+
self.object_size_cache[key_str] = obj_size
|
|
433
|
+
|
|
434
|
+
memory_obj = self.local_cpu_backend.allocate(
|
|
435
|
+
self.meta_shapes,
|
|
436
|
+
self.meta_dtypes,
|
|
437
|
+
self.meta_fmt,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if not memory_obj:
|
|
441
|
+
memory_objs.append(None)
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
# Check if stored size matches expected size
|
|
445
|
+
if obj_size != memory_obj.get_size():
|
|
446
|
+
logger.error(
|
|
447
|
+
f"Size mismatch for {key_str}: S3 has {obj_size} bytes, "
|
|
448
|
+
f"but current config expects {memory_obj.get_size()} bytes. "
|
|
449
|
+
f"Skipping this key."
|
|
450
|
+
)
|
|
451
|
+
memory_obj.ref_count_down()
|
|
452
|
+
memory_objs.append(None)
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
memory_objs.append(memory_obj)
|
|
456
|
+
|
|
457
|
+
s3_req = self._s3_download(
|
|
458
|
+
key_str=key_str,
|
|
459
|
+
mem_obj=memory_obj,
|
|
460
|
+
)
|
|
461
|
+
fut = asyncio.wrap_future(s3_req.finished_future)
|
|
462
|
+
futures.append(fut)
|
|
463
|
+
future_to_memobj_idx.append(len(memory_objs) - 1)
|
|
464
|
+
|
|
465
|
+
# Use return_exceptions to prevent one failure from stopping all downloads
|
|
466
|
+
results = await asyncio.gather(*futures, return_exceptions=True)
|
|
467
|
+
|
|
468
|
+
had_success = False
|
|
469
|
+
|
|
470
|
+
for future_idx, result in enumerate(results):
|
|
471
|
+
memobj_idx = future_to_memobj_idx[future_idx]
|
|
472
|
+
|
|
473
|
+
if isinstance(result, Exception):
|
|
474
|
+
error_msg = str(result)
|
|
475
|
+
|
|
476
|
+
is_connection_error = self._update_connection_failures(error_msg)
|
|
477
|
+
|
|
478
|
+
if not is_connection_error:
|
|
479
|
+
# Log non-connection errors
|
|
480
|
+
logger.error(
|
|
481
|
+
f"Failed to download key at index {memobj_idx}: {error_msg}"
|
|
482
|
+
)
|
|
483
|
+
# Release the memory object for failed download
|
|
484
|
+
memobj = memory_objs[memobj_idx]
|
|
485
|
+
if memobj is not None:
|
|
486
|
+
memobj.ref_count_down()
|
|
487
|
+
memory_objs[memobj_idx] = None
|
|
488
|
+
else:
|
|
489
|
+
had_success = True
|
|
490
|
+
|
|
491
|
+
if had_success:
|
|
492
|
+
self._reset_connection_failures()
|
|
493
|
+
|
|
494
|
+
return memory_objs
|
|
495
|
+
|
|
496
|
+
def _s3_upload(
|
|
497
|
+
self,
|
|
498
|
+
key_str: str,
|
|
499
|
+
memory_obj: MemoryObj,
|
|
500
|
+
) -> "s3.S3Request":
|
|
501
|
+
"""
|
|
502
|
+
Upload a file to S3.
|
|
503
|
+
"""
|
|
504
|
+
# Zero-copy approach using MemoryViewStream
|
|
505
|
+
stream = MemoryViewStream(memory_obj.byte_array)
|
|
506
|
+
# Calculate total length from the memoryview
|
|
507
|
+
total_len = len(stream)
|
|
508
|
+
|
|
509
|
+
headers = HttpHeaders()
|
|
510
|
+
headers.add("Host", self.s3_endpoint)
|
|
511
|
+
headers.add("Content-Length", str(total_len))
|
|
512
|
+
headers.add("Content-Type", "application/octet-stream")
|
|
513
|
+
|
|
514
|
+
req = HttpRequest(
|
|
515
|
+
"PUT", self._format_safe_path(key_str), headers, body_stream=stream
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
done = {"err": None, "status": None}
|
|
519
|
+
|
|
520
|
+
def on_done(error=None, status_code=None, **kwargs):
|
|
521
|
+
done["err"] = error
|
|
522
|
+
done["status"] = status_code
|
|
523
|
+
|
|
524
|
+
if done["err"] or done["status"] not in (200, 201):
|
|
525
|
+
raise RuntimeError(f"Upload failed in S3Connector: {done}")
|
|
526
|
+
|
|
527
|
+
s3_req = s3.S3Request(
|
|
528
|
+
client=self.s3_client,
|
|
529
|
+
type=s3.S3RequestType.PUT_OBJECT,
|
|
530
|
+
request=req,
|
|
531
|
+
credential_provider=self.credentials_provider,
|
|
532
|
+
region=self.s3_region,
|
|
533
|
+
on_done=on_done,
|
|
534
|
+
)
|
|
535
|
+
return s3_req
|
|
536
|
+
|
|
537
|
+
async def _put(self, key: CacheEngineKey, memory_obj: MemoryObj):
|
|
538
|
+
"""
|
|
539
|
+
Store data to S3
|
|
540
|
+
"""
|
|
541
|
+
# Circuit breaker: if connection is disabled, just log and return
|
|
542
|
+
if self.connection_disabled:
|
|
543
|
+
logger.debug(
|
|
544
|
+
f"S3 connection disabled due to repeated failures. "
|
|
545
|
+
f"Skipping upload for {key.to_string()}"
|
|
546
|
+
)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
key_str = key.to_string()
|
|
550
|
+
|
|
551
|
+
# Check if the chunk size matches expected S3 part size
|
|
552
|
+
if memory_obj.get_physical_size() != self.s3_part_size:
|
|
553
|
+
logger.error(
|
|
554
|
+
f"Cannot upload {key_str}: chunk size {memory_obj.get_physical_size()} "
|
|
555
|
+
f"bytes does not match S3 part size {self.s3_part_size} bytes. "
|
|
556
|
+
f"Partial/unfull chunks are not supported."
|
|
557
|
+
)
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
logger.debug(f"Uploading {key_str} to S3")
|
|
562
|
+
s3_req = self._s3_upload(key_str, memory_obj)
|
|
563
|
+
await asyncio.wrap_future(s3_req.finished_future)
|
|
564
|
+
|
|
565
|
+
self.object_size_cache[key_str] = memory_obj.get_physical_size()
|
|
566
|
+
logger.debug(f"Uploaded {key_str} to S3 successfully")
|
|
567
|
+
|
|
568
|
+
# Reset failure counter on success
|
|
569
|
+
self._reset_connection_failures()
|
|
570
|
+
except Exception as e:
|
|
571
|
+
error_msg = str(e)
|
|
572
|
+
|
|
573
|
+
# Update connection failures and check if it's a connection error
|
|
574
|
+
is_connection_error = self._update_connection_failures(error_msg)
|
|
575
|
+
|
|
576
|
+
if not is_connection_error:
|
|
577
|
+
# Log non-connection errors
|
|
578
|
+
logger.error(f"Failed to upload {key_str} to S3: {e}")
|
|
579
|
+
|
|
580
|
+
async def put(self, key: CacheEngineKey, memory_obj: MemoryObj):
|
|
581
|
+
return await self.pq_executor.submit_job(
|
|
582
|
+
self._put,
|
|
583
|
+
key=key,
|
|
584
|
+
memory_obj=memory_obj,
|
|
585
|
+
priority=Priorities.PUT,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def support_batched_async_contains(self) -> bool:
|
|
589
|
+
return True
|
|
590
|
+
|
|
591
|
+
async def _batched_async_contains(
|
|
592
|
+
self, lookup_id: str, keys: List[CacheEngineKey], pin: bool = False
|
|
593
|
+
) -> int:
|
|
594
|
+
# Circuit breaker: if connection is disabled, return 0
|
|
595
|
+
if self.connection_disabled:
|
|
596
|
+
return 0
|
|
597
|
+
|
|
598
|
+
num_hit_counts = 0
|
|
599
|
+
for key in keys:
|
|
600
|
+
key_str = key.to_string()
|
|
601
|
+
cached_size = self.object_size_cache.get(key_str, None)
|
|
602
|
+
if cached_size is not None:
|
|
603
|
+
if cached_size > 0:
|
|
604
|
+
num_hit_counts += 1
|
|
605
|
+
continue
|
|
606
|
+
else:
|
|
607
|
+
return num_hit_counts
|
|
608
|
+
|
|
609
|
+
obj_size = await self._get_object_size_async(key_str)
|
|
610
|
+
if not obj_size > 0:
|
|
611
|
+
self.object_size_cache[key_str] = 0
|
|
612
|
+
return num_hit_counts
|
|
613
|
+
|
|
614
|
+
self.object_size_cache[key_str] = obj_size
|
|
615
|
+
num_hit_counts += 1
|
|
616
|
+
|
|
617
|
+
return num_hit_counts
|
|
618
|
+
|
|
619
|
+
async def batched_async_contains(
|
|
620
|
+
self, lookup_id: str, keys: List[CacheEngineKey], pin: bool = False
|
|
621
|
+
) -> int:
|
|
622
|
+
return await self.pq_executor.submit_job(
|
|
623
|
+
self._batched_async_contains,
|
|
624
|
+
lookup_id=lookup_id,
|
|
625
|
+
keys=keys,
|
|
626
|
+
pin=pin,
|
|
627
|
+
priority=Priorities.PEEK,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def support_batched_get_non_blocking(self) -> bool:
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
async def _batched_get_non_blocking(
|
|
634
|
+
self,
|
|
635
|
+
lookup_id: str,
|
|
636
|
+
keys: List[CacheEngineKey],
|
|
637
|
+
) -> List[MemoryObj]:
|
|
638
|
+
# batched get is already a coroutine
|
|
639
|
+
result = await self.batched_get(keys)
|
|
640
|
+
return [r for r in result if r is not None]
|
|
641
|
+
|
|
642
|
+
async def batched_get_non_blocking(
|
|
643
|
+
self, lookup_id: str, keys: List[CacheEngineKey]
|
|
644
|
+
) -> List[MemoryObj]:
|
|
645
|
+
return await self.pq_executor.submit_job(
|
|
646
|
+
self._batched_get_non_blocking,
|
|
647
|
+
lookup_id=lookup_id,
|
|
648
|
+
keys=keys,
|
|
649
|
+
priority=Priorities.PREFETCH,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
async def list(self) -> List[str]:
|
|
653
|
+
raise NotImplementedError
|
|
654
|
+
|
|
655
|
+
def support_ping(self) -> bool:
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
# TODO(Jiayi): This needs to be implemented.
|
|
659
|
+
async def ping(self) -> int:
|
|
660
|
+
raise NotImplementedError
|
|
661
|
+
|
|
662
|
+
def support_batched_get(self) -> bool:
|
|
663
|
+
return True
|
|
664
|
+
|
|
665
|
+
def _update_connection_failures(self, error_msg: str) -> bool:
|
|
666
|
+
# Check if it's a connection error
|
|
667
|
+
is_connection_error = (
|
|
668
|
+
"CONNECTION_REFUSED" in error_msg
|
|
669
|
+
or "SOCKET" in error_msg
|
|
670
|
+
or "DNS" in error_msg
|
|
671
|
+
or "TIMEOUT" in error_msg
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if is_connection_error:
|
|
675
|
+
self.connection_failures += 1
|
|
676
|
+
logger.error(
|
|
677
|
+
f"S3 connection error ({self.connection_failures}/"
|
|
678
|
+
f"{self.max_connection_failures}): {error_msg}"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
if self.connection_failures >= self.max_connection_failures:
|
|
682
|
+
self.connection_disabled = True
|
|
683
|
+
logger.error(
|
|
684
|
+
f"S3 connection disabled after "
|
|
685
|
+
f"{self.max_connection_failures} "
|
|
686
|
+
f"consecutive failures. "
|
|
687
|
+
f"All future S3 operations will be skipped."
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
return is_connection_error
|
|
691
|
+
|
|
692
|
+
def _reset_connection_failures(self):
|
|
693
|
+
"""Reset connection failure counter on successful operation."""
|
|
694
|
+
if self.connection_failures > 0:
|
|
695
|
+
logger.info("S3 connection recovered")
|
|
696
|
+
self.connection_failures = 0
|
|
697
|
+
|
|
698
|
+
async def close(self):
|
|
699
|
+
await self.pq_executor.shutdown(wait=True)
|