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.
Files changed (399) hide show
  1. lmcache/__init__.py +84 -0
  2. lmcache/_version.py +24 -0
  3. lmcache/cli/__init__.py +1 -0
  4. lmcache/cli/commands/__init__.py +34 -0
  5. lmcache/cli/commands/base.py +157 -0
  6. lmcache/cli/commands/bench/__init__.py +557 -0
  7. lmcache/cli/commands/bench/engine_bench/__init__.py +1 -0
  8. lmcache/cli/commands/bench/engine_bench/config.py +245 -0
  9. lmcache/cli/commands/bench/engine_bench/interactive/__init__.py +274 -0
  10. lmcache/cli/commands/bench/engine_bench/interactive/config.json +10 -0
  11. lmcache/cli/commands/bench/engine_bench/interactive/schema.py +352 -0
  12. lmcache/cli/commands/bench/engine_bench/interactive/state.py +327 -0
  13. lmcache/cli/commands/bench/engine_bench/interactive/terminal.py +291 -0
  14. lmcache/cli/commands/bench/engine_bench/progress.py +145 -0
  15. lmcache/cli/commands/bench/engine_bench/request_sender.py +232 -0
  16. lmcache/cli/commands/bench/engine_bench/stats.py +275 -0
  17. lmcache/cli/commands/bench/engine_bench/workloads/__init__.py +153 -0
  18. lmcache/cli/commands/bench/engine_bench/workloads/base.py +122 -0
  19. lmcache/cli/commands/bench/engine_bench/workloads/long_doc_permutator.py +435 -0
  20. lmcache/cli/commands/bench/engine_bench/workloads/long_doc_qa.py +281 -0
  21. lmcache/cli/commands/bench/engine_bench/workloads/multi_round_chat.py +337 -0
  22. lmcache/cli/commands/bench/engine_bench/workloads/random_prefill.py +178 -0
  23. lmcache/cli/commands/describe.py +310 -0
  24. lmcache/cli/commands/kvcache.py +133 -0
  25. lmcache/cli/commands/mock.py +75 -0
  26. lmcache/cli/commands/ping.py +113 -0
  27. lmcache/cli/commands/query/__init__.py +155 -0
  28. lmcache/cli/commands/query/prompt.py +134 -0
  29. lmcache/cli/commands/query/request.py +357 -0
  30. lmcache/cli/commands/server.py +99 -0
  31. lmcache/cli/commands/tool/__init__.py +63 -0
  32. lmcache/cli/commands/tool/cache_simulator.py +113 -0
  33. lmcache/cli/commands/trace/__init__.py +505 -0
  34. lmcache/cli/commands/trace/dispatch.py +249 -0
  35. lmcache/cli/commands/trace/driver.py +372 -0
  36. lmcache/cli/commands/trace/stats.py +289 -0
  37. lmcache/cli/documents/lmcache.txt +11 -0
  38. lmcache/cli/main.py +42 -0
  39. lmcache/cli/metrics/__init__.py +29 -0
  40. lmcache/cli/metrics/formatter.py +171 -0
  41. lmcache/cli/metrics/handler.py +94 -0
  42. lmcache/cli/metrics/metrics.py +161 -0
  43. lmcache/cli/metrics/section.py +77 -0
  44. lmcache/connections.py +173 -0
  45. lmcache/integration/__init__.py +2 -0
  46. lmcache/integration/base_service_factory.py +165 -0
  47. lmcache/integration/request_telemetry/__init__.py +1 -0
  48. lmcache/integration/request_telemetry/base.py +51 -0
  49. lmcache/integration/request_telemetry/factory.py +113 -0
  50. lmcache/integration/request_telemetry/fastapi.py +109 -0
  51. lmcache/integration/request_telemetry/noop.py +35 -0
  52. lmcache/integration/sglang/__init__.py +2 -0
  53. lmcache/integration/sglang/sglang_adapter.py +326 -0
  54. lmcache/integration/sglang/utils.py +39 -0
  55. lmcache/integration/vllm/__init__.py +1 -0
  56. lmcache/integration/vllm/lmcache_connector_v1.py +213 -0
  57. lmcache/integration/vllm/lmcache_connector_v1_085.py +150 -0
  58. lmcache/integration/vllm/lmcache_mp_connector_0180.py +1072 -0
  59. lmcache/integration/vllm/tests/test_mm_hash_utils.py +112 -0
  60. lmcache/integration/vllm/utils.py +433 -0
  61. lmcache/integration/vllm/vllm_multi_process_adapter.py +1090 -0
  62. lmcache/integration/vllm/vllm_service_factory.py +339 -0
  63. lmcache/integration/vllm/vllm_v1_adapter.py +1713 -0
  64. lmcache/logging.py +107 -0
  65. lmcache/native_storage_ops.pyi +230 -0
  66. lmcache/non_cuda_equivalents.py +1424 -0
  67. lmcache/observability.py +1958 -0
  68. lmcache/storage_backend/serde/__init__.py +1 -0
  69. lmcache/storage_backend/serde/cachegen_basics.py +210 -0
  70. lmcache/storage_backend/serde/cachegen_decoder.py +207 -0
  71. lmcache/storage_backend/serde/cachegen_encoder.py +394 -0
  72. lmcache/storage_backend/serde/serde.py +75 -0
  73. lmcache/tools/__init__.py +1 -0
  74. lmcache/tools/cache_simulator/README.md +392 -0
  75. lmcache/tools/cache_simulator/__init__.py +1 -0
  76. lmcache/tools/cache_simulator/docs/simulate_example.png +0 -0
  77. lmcache/tools/cache_simulator/docs/sweep_example.png +0 -0
  78. lmcache/tools/cache_simulator/gen_bench_dataset.py +360 -0
  79. lmcache/tools/cache_simulator/lru_cache.py +124 -0
  80. lmcache/tools/cache_simulator/plot_hit_rate.py +231 -0
  81. lmcache/tools/cache_simulator/simulator.py +795 -0
  82. lmcache/tools/controller_benchmark/README.md +161 -0
  83. lmcache/tools/controller_benchmark/__init__.py +1 -0
  84. lmcache/tools/controller_benchmark/__main__.py +331 -0
  85. lmcache/tools/controller_benchmark/benchmark.py +660 -0
  86. lmcache/tools/controller_benchmark/config.py +44 -0
  87. lmcache/tools/controller_benchmark/constants.py +10 -0
  88. lmcache/tools/controller_benchmark/handlers/__init__.py +46 -0
  89. lmcache/tools/controller_benchmark/handlers/admit.py +52 -0
  90. lmcache/tools/controller_benchmark/handlers/base.py +47 -0
  91. lmcache/tools/controller_benchmark/handlers/deregister.py +49 -0
  92. lmcache/tools/controller_benchmark/handlers/evict.py +52 -0
  93. lmcache/tools/controller_benchmark/handlers/heartbeat.py +56 -0
  94. lmcache/tools/controller_benchmark/handlers/p2p_lookup.py +47 -0
  95. lmcache/tools/controller_benchmark/handlers/register.py +56 -0
  96. lmcache/tools/mp_status_viewer/__init__.py +1 -0
  97. lmcache/tools/mp_status_viewer/__main__.py +95 -0
  98. lmcache/usage_context.py +417 -0
  99. lmcache/utils.py +665 -0
  100. lmcache/v1/__init__.py +2 -0
  101. lmcache/v1/api_server/__init__.py +2 -0
  102. lmcache/v1/api_server/__main__.py +537 -0
  103. lmcache/v1/basic_check.py +112 -0
  104. lmcache/v1/cache_controller/__init__.py +9 -0
  105. lmcache/v1/cache_controller/commands/__init__.py +15 -0
  106. lmcache/v1/cache_controller/commands/base.py +35 -0
  107. lmcache/v1/cache_controller/commands/full_sync.py +49 -0
  108. lmcache/v1/cache_controller/config.py +176 -0
  109. lmcache/v1/cache_controller/controller_manager.py +535 -0
  110. lmcache/v1/cache_controller/controllers/__init__.py +11 -0
  111. lmcache/v1/cache_controller/controllers/full_sync_tracker.py +473 -0
  112. lmcache/v1/cache_controller/controllers/kv_controller.py +439 -0
  113. lmcache/v1/cache_controller/controllers/registration_controller.py +282 -0
  114. lmcache/v1/cache_controller/executor.py +463 -0
  115. lmcache/v1/cache_controller/frontend/static/css/style.css +201 -0
  116. lmcache/v1/cache_controller/frontend/static/img/logo.png +0 -0
  117. lmcache/v1/cache_controller/frontend/static/index.html +234 -0
  118. lmcache/v1/cache_controller/frontend/static/js/controller_app.js +660 -0
  119. lmcache/v1/cache_controller/full_sync_sender.py +475 -0
  120. lmcache/v1/cache_controller/locks.py +149 -0
  121. lmcache/v1/cache_controller/message.py +828 -0
  122. lmcache/v1/cache_controller/observability.py +208 -0
  123. lmcache/v1/cache_controller/utils.py +679 -0
  124. lmcache/v1/cache_controller/worker.py +665 -0
  125. lmcache/v1/cache_engine.py +2058 -0
  126. lmcache/v1/cache_interface.py +19 -0
  127. lmcache/v1/check/__init__.py +74 -0
  128. lmcache/v1/check/check_mode_gen.py +86 -0
  129. lmcache/v1/check/check_mode_test_l2_adapter.py +284 -0
  130. lmcache/v1/check/check_mode_test_remote.py +155 -0
  131. lmcache/v1/check/check_mode_test_storage_manager.py +142 -0
  132. lmcache/v1/check/utils.py +571 -0
  133. lmcache/v1/compute/__init__.py +2 -0
  134. lmcache/v1/compute/attention/__init__.py +0 -0
  135. lmcache/v1/compute/attention/abstract.py +39 -0
  136. lmcache/v1/compute/attention/flash_attn.py +129 -0
  137. lmcache/v1/compute/attention/flash_infer_sparse.py +284 -0
  138. lmcache/v1/compute/attention/metadata.py +85 -0
  139. lmcache/v1/compute/attention/utils.py +14 -0
  140. lmcache/v1/compute/blend/__init__.py +7 -0
  141. lmcache/v1/compute/blend/blender.py +168 -0
  142. lmcache/v1/compute/blend/metadata.py +34 -0
  143. lmcache/v1/compute/blend/utils.py +63 -0
  144. lmcache/v1/compute/models/__init__.py +0 -0
  145. lmcache/v1/compute/models/base.py +141 -0
  146. lmcache/v1/compute/models/llama.py +9 -0
  147. lmcache/v1/compute/models/qwen3.py +24 -0
  148. lmcache/v1/compute/models/utils.py +68 -0
  149. lmcache/v1/compute/positional_encoding.py +199 -0
  150. lmcache/v1/config.py +848 -0
  151. lmcache/v1/config_base.py +848 -0
  152. lmcache/v1/distributed/api.py +248 -0
  153. lmcache/v1/distributed/config.py +321 -0
  154. lmcache/v1/distributed/error.py +64 -0
  155. lmcache/v1/distributed/eviction.py +192 -0
  156. lmcache/v1/distributed/eviction_policy/__init__.py +21 -0
  157. lmcache/v1/distributed/eviction_policy/factory.py +27 -0
  158. lmcache/v1/distributed/eviction_policy/lru.py +244 -0
  159. lmcache/v1/distributed/eviction_policy/noop.py +50 -0
  160. lmcache/v1/distributed/internal_api.py +170 -0
  161. lmcache/v1/distributed/l1_manager.py +835 -0
  162. lmcache/v1/distributed/l2_adapters/__init__.py +67 -0
  163. lmcache/v1/distributed/l2_adapters/base.py +360 -0
  164. lmcache/v1/distributed/l2_adapters/config.py +385 -0
  165. lmcache/v1/distributed/l2_adapters/factory.py +205 -0
  166. lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +747 -0
  167. lmcache/v1/distributed/l2_adapters/fs_native_l2_adapter.py +167 -0
  168. lmcache/v1/distributed/l2_adapters/mock_l2_adapter.py +516 -0
  169. lmcache/v1/distributed/l2_adapters/mooncake_store_l2_adapter.py +135 -0
  170. lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py +468 -0
  171. lmcache/v1/distributed/l2_adapters/native_plugin_l2_adapter.py +199 -0
  172. lmcache/v1/distributed/l2_adapters/nixl_store_dynamic_l2_adapter.py +831 -0
  173. lmcache/v1/distributed/l2_adapters/nixl_store_l2_adapter.py +983 -0
  174. lmcache/v1/distributed/l2_adapters/plugin_l2_adapter.py +210 -0
  175. lmcache/v1/distributed/l2_adapters/resp_l2_adapter.py +176 -0
  176. lmcache/v1/distributed/memory_manager.py +179 -0
  177. lmcache/v1/distributed/storage_controller.py +39 -0
  178. lmcache/v1/distributed/storage_controllers/__init__.py +43 -0
  179. lmcache/v1/distributed/storage_controllers/eviction_controller.py +242 -0
  180. lmcache/v1/distributed/storage_controllers/prefetch_controller.py +830 -0
  181. lmcache/v1/distributed/storage_controllers/prefetch_policy.py +193 -0
  182. lmcache/v1/distributed/storage_controllers/store_controller.py +452 -0
  183. lmcache/v1/distributed/storage_controllers/store_policy.py +213 -0
  184. lmcache/v1/distributed/storage_manager.py +532 -0
  185. lmcache/v1/event_manager.py +145 -0
  186. lmcache/v1/exceptions/__init__.py +16 -0
  187. lmcache/v1/gpu_connector/__init__.py +126 -0
  188. lmcache/v1/gpu_connector/gpu_connectors.py +1906 -0
  189. lmcache/v1/gpu_connector/gpu_ops.py +85 -0
  190. lmcache/v1/gpu_connector/hpu_connector.py +326 -0
  191. lmcache/v1/gpu_connector/mock_gpu_connector.py +67 -0
  192. lmcache/v1/gpu_connector/utils.py +890 -0
  193. lmcache/v1/gpu_connector/xpu_connectors.py +916 -0
  194. lmcache/v1/health_monitor/__init__.py +1 -0
  195. lmcache/v1/health_monitor/base.py +587 -0
  196. lmcache/v1/health_monitor/checks/__init__.py +1 -0
  197. lmcache/v1/health_monitor/checks/remote_backend_check.py +304 -0
  198. lmcache/v1/health_monitor/constants.py +36 -0
  199. lmcache/v1/internal_api_server/__init__.py +0 -0
  200. lmcache/v1/internal_api_server/api_registry.py +59 -0
  201. lmcache/v1/internal_api_server/api_server.py +120 -0
  202. lmcache/v1/internal_api_server/common/__init__.py +1 -0
  203. lmcache/v1/internal_api_server/common/env_api.py +22 -0
  204. lmcache/v1/internal_api_server/common/loglevel_api.py +57 -0
  205. lmcache/v1/internal_api_server/common/metrics_api.py +29 -0
  206. lmcache/v1/internal_api_server/common/periodic_thread_api.py +138 -0
  207. lmcache/v1/internal_api_server/common/run_script_api.py +73 -0
  208. lmcache/v1/internal_api_server/common/thread_api.py +63 -0
  209. lmcache/v1/internal_api_server/controller/__init__.py +1 -0
  210. lmcache/v1/internal_api_server/controller/key_stats_api.py +81 -0
  211. lmcache/v1/internal_api_server/controller/worker_info_api.py +136 -0
  212. lmcache/v1/internal_api_server/utils.py +43 -0
  213. lmcache/v1/internal_api_server/vllm/__init__.py +1 -0
  214. lmcache/v1/internal_api_server/vllm/backend_api.py +221 -0
  215. lmcache/v1/internal_api_server/vllm/bypass_api.py +204 -0
  216. lmcache/v1/internal_api_server/vllm/cache_api.py +895 -0
  217. lmcache/v1/internal_api_server/vllm/chunk_statistics_api.py +141 -0
  218. lmcache/v1/internal_api_server/vllm/conf_api.py +147 -0
  219. lmcache/v1/internal_api_server/vllm/freeze_api.py +172 -0
  220. lmcache/v1/internal_api_server/vllm/hot_cache_api.py +184 -0
  221. lmcache/v1/internal_api_server/vllm/inference_api.py +65 -0
  222. lmcache/v1/internal_api_server/vllm/load_fs_chunks_api.py +320 -0
  223. lmcache/v1/internal_api_server/vllm/lookup_api.py +145 -0
  224. lmcache/v1/internal_api_server/vllm/version_api.py +25 -0
  225. lmcache/v1/kv_layer_groups.py +267 -0
  226. lmcache/v1/lazy_memory_allocator.py +284 -0
  227. lmcache/v1/lookup_client/__init__.py +25 -0
  228. lmcache/v1/lookup_client/abstract_client.py +77 -0
  229. lmcache/v1/lookup_client/async_lookup_message.py +50 -0
  230. lmcache/v1/lookup_client/chunk_statistics_lookup_client.py +200 -0
  231. lmcache/v1/lookup_client/factory.py +251 -0
  232. lmcache/v1/lookup_client/hit_limit_lookup_client.py +86 -0
  233. lmcache/v1/lookup_client/lmcache_async_lookup_client.py +407 -0
  234. lmcache/v1/lookup_client/lmcache_lookup_client.py +285 -0
  235. lmcache/v1/lookup_client/lmcache_lookup_client_bypass.py +99 -0
  236. lmcache/v1/lookup_client/mooncake_lookup_client.py +87 -0
  237. lmcache/v1/lookup_client/record_strategies/__init__.py +77 -0
  238. lmcache/v1/lookup_client/record_strategies/base.py +327 -0
  239. lmcache/v1/lookup_client/record_strategies/file_hash.py +130 -0
  240. lmcache/v1/lookup_client/record_strategies/memory_bloom_filter.py +81 -0
  241. lmcache/v1/manager.py +539 -0
  242. lmcache/v1/memory_management.py +2619 -0
  243. lmcache/v1/metadata.py +114 -0
  244. lmcache/v1/mp_observability/AGENTS.override.md +21 -0
  245. lmcache/v1/mp_observability/README.md +204 -0
  246. lmcache/v1/mp_observability/config.py +340 -0
  247. lmcache/v1/mp_observability/event.py +100 -0
  248. lmcache/v1/mp_observability/event_bus.py +313 -0
  249. lmcache/v1/mp_observability/otel_init.py +145 -0
  250. lmcache/v1/mp_observability/subscribers/__init__.py +28 -0
  251. lmcache/v1/mp_observability/subscribers/logging/__init__.py +19 -0
  252. lmcache/v1/mp_observability/subscribers/logging/l1.py +56 -0
  253. lmcache/v1/mp_observability/subscribers/logging/l2.py +73 -0
  254. lmcache/v1/mp_observability/subscribers/logging/lookup_hash.py +209 -0
  255. lmcache/v1/mp_observability/subscribers/logging/mp_server.py +90 -0
  256. lmcache/v1/mp_observability/subscribers/logging/sm.py +59 -0
  257. lmcache/v1/mp_observability/subscribers/metrics/__init__.py +20 -0
  258. lmcache/v1/mp_observability/subscribers/metrics/l0_lifecycle.py +290 -0
  259. lmcache/v1/mp_observability/subscribers/metrics/l1.py +55 -0
  260. lmcache/v1/mp_observability/subscribers/metrics/l1_lifecycle.py +166 -0
  261. lmcache/v1/mp_observability/subscribers/metrics/l2.py +121 -0
  262. lmcache/v1/mp_observability/subscribers/metrics/sm.py +69 -0
  263. lmcache/v1/mp_observability/subscribers/tracing/__init__.py +12 -0
  264. lmcache/v1/mp_observability/subscribers/tracing/mp_server.py +333 -0
  265. lmcache/v1/mp_observability/subscribers/tracing/span_registry.py +148 -0
  266. lmcache/v1/mp_observability/trace/__init__.py +50 -0
  267. lmcache/v1/mp_observability/trace/codecs.py +255 -0
  268. lmcache/v1/mp_observability/trace/decorator.py +147 -0
  269. lmcache/v1/mp_observability/trace/format.py +132 -0
  270. lmcache/v1/mp_observability/trace/lifecycle.py +83 -0
  271. lmcache/v1/mp_observability/trace/reader.py +167 -0
  272. lmcache/v1/mp_observability/trace/recorder.py +300 -0
  273. lmcache/v1/multiprocess/__init__.py +0 -0
  274. lmcache/v1/multiprocess/affinity_pool.py +102 -0
  275. lmcache/v1/multiprocess/blend_server_v2.py +891 -0
  276. lmcache/v1/multiprocess/config.py +253 -0
  277. lmcache/v1/multiprocess/custom_types.py +281 -0
  278. lmcache/v1/multiprocess/futures.py +194 -0
  279. lmcache/v1/multiprocess/gpu_context.py +511 -0
  280. lmcache/v1/multiprocess/http_server.py +235 -0
  281. lmcache/v1/multiprocess/mp_runtime_plugin_launcher.py +130 -0
  282. lmcache/v1/multiprocess/mq.py +732 -0
  283. lmcache/v1/multiprocess/protocol.py +86 -0
  284. lmcache/v1/multiprocess/protocols/README.md +213 -0
  285. lmcache/v1/multiprocess/protocols/__init__.py +127 -0
  286. lmcache/v1/multiprocess/protocols/base.py +89 -0
  287. lmcache/v1/multiprocess/protocols/blend.py +109 -0
  288. lmcache/v1/multiprocess/protocols/blend_v2.py +57 -0
  289. lmcache/v1/multiprocess/protocols/controller.py +53 -0
  290. lmcache/v1/multiprocess/protocols/debug.py +34 -0
  291. lmcache/v1/multiprocess/protocols/engine.py +146 -0
  292. lmcache/v1/multiprocess/protocols/observability.py +39 -0
  293. lmcache/v1/multiprocess/server.py +1134 -0
  294. lmcache/v1/multiprocess/session.py +190 -0
  295. lmcache/v1/multiprocess/token_hasher.py +441 -0
  296. lmcache/v1/offload_server/__init__.py +17 -0
  297. lmcache/v1/offload_server/abstract_server.py +37 -0
  298. lmcache/v1/offload_server/message.py +30 -0
  299. lmcache/v1/offload_server/zmq_server.py +122 -0
  300. lmcache/v1/periodic_thread.py +579 -0
  301. lmcache/v1/pin_monitor.py +246 -0
  302. lmcache/v1/plugin/__init__.py +0 -0
  303. lmcache/v1/plugin/runtime_plugin_launcher.py +211 -0
  304. lmcache/v1/protocol.py +317 -0
  305. lmcache/v1/rpc/__init__.py +17 -0
  306. lmcache/v1/rpc/transport.py +105 -0
  307. lmcache/v1/rpc/zmq_transport.py +213 -0
  308. lmcache/v1/rpc_utils.py +165 -0
  309. lmcache/v1/server/__init__.py +2 -0
  310. lmcache/v1/server/__main__.py +170 -0
  311. lmcache/v1/server/storage_backend/__init__.py +21 -0
  312. lmcache/v1/server/storage_backend/abstract_backend.py +80 -0
  313. lmcache/v1/server/storage_backend/local_backend.py +75 -0
  314. lmcache/v1/server/utils.py +21 -0
  315. lmcache/v1/standalone/__init__.py +1 -0
  316. lmcache/v1/standalone/__main__.py +583 -0
  317. lmcache/v1/standalone/manager.py +80 -0
  318. lmcache/v1/standalone/standalone_service_factory.py +86 -0
  319. lmcache/v1/storage_backend/__init__.py +313 -0
  320. lmcache/v1/storage_backend/abstract_backend.py +445 -0
  321. lmcache/v1/storage_backend/audit_backend.py +233 -0
  322. lmcache/v1/storage_backend/batched_message_sender.py +222 -0
  323. lmcache/v1/storage_backend/cache_policy/__init__.py +45 -0
  324. lmcache/v1/storage_backend/cache_policy/base_policy.py +87 -0
  325. lmcache/v1/storage_backend/cache_policy/fifo.py +58 -0
  326. lmcache/v1/storage_backend/cache_policy/lfu.py +105 -0
  327. lmcache/v1/storage_backend/cache_policy/lru.py +81 -0
  328. lmcache/v1/storage_backend/cache_policy/mru.py +61 -0
  329. lmcache/v1/storage_backend/connector/__init__.py +443 -0
  330. lmcache/v1/storage_backend/connector/audit_adapter.py +77 -0
  331. lmcache/v1/storage_backend/connector/audit_connector.py +320 -0
  332. lmcache/v1/storage_backend/connector/base_connector.py +379 -0
  333. lmcache/v1/storage_backend/connector/blackhole_adapter.py +21 -0
  334. lmcache/v1/storage_backend/connector/blackhole_connector.py +37 -0
  335. lmcache/v1/storage_backend/connector/eic_adapter.py +31 -0
  336. lmcache/v1/storage_backend/connector/eic_connector.py +757 -0
  337. lmcache/v1/storage_backend/connector/external_adapter.py +79 -0
  338. lmcache/v1/storage_backend/connector/fs_adapter.py +51 -0
  339. lmcache/v1/storage_backend/connector/fs_connector.py +403 -0
  340. lmcache/v1/storage_backend/connector/infinistore_adapter.py +56 -0
  341. lmcache/v1/storage_backend/connector/infinistore_connector.py +177 -0
  342. lmcache/v1/storage_backend/connector/instrumented_connector.py +219 -0
  343. lmcache/v1/storage_backend/connector/lm_adapter.py +31 -0
  344. lmcache/v1/storage_backend/connector/lm_connector.py +176 -0
  345. lmcache/v1/storage_backend/connector/mock_adapter.py +57 -0
  346. lmcache/v1/storage_backend/connector/mock_connector.py +349 -0
  347. lmcache/v1/storage_backend/connector/mooncakestore_adapter.py +43 -0
  348. lmcache/v1/storage_backend/connector/mooncakestore_connector.py +614 -0
  349. lmcache/v1/storage_backend/connector/redis_adapter.py +181 -0
  350. lmcache/v1/storage_backend/connector/redis_connector.py +828 -0
  351. lmcache/v1/storage_backend/connector/s3_adapter.py +59 -0
  352. lmcache/v1/storage_backend/connector/s3_connector.py +699 -0
  353. lmcache/v1/storage_backend/connector/sagemaker_hyperpod_adapter.py +233 -0
  354. lmcache/v1/storage_backend/connector/sagemaker_hyperpod_connector.py +987 -0
  355. lmcache/v1/storage_backend/connector/valkey_adapter.py +114 -0
  356. lmcache/v1/storage_backend/connector/valkey_connector.py +627 -0
  357. lmcache/v1/storage_backend/gds_backend.py +1199 -0
  358. lmcache/v1/storage_backend/job_executor/__init__.py +0 -0
  359. lmcache/v1/storage_backend/job_executor/base_executor.py +34 -0
  360. lmcache/v1/storage_backend/job_executor/pq_executor.py +235 -0
  361. lmcache/v1/storage_backend/local_cpu_backend.py +810 -0
  362. lmcache/v1/storage_backend/local_disk_backend.py +656 -0
  363. lmcache/v1/storage_backend/maru_backend.py +734 -0
  364. lmcache/v1/storage_backend/naive_serde/__init__.py +50 -0
  365. lmcache/v1/storage_backend/naive_serde/cachegen_basics.py +133 -0
  366. lmcache/v1/storage_backend/naive_serde/cachegen_decoder.py +135 -0
  367. lmcache/v1/storage_backend/naive_serde/cachegen_encoder.py +83 -0
  368. lmcache/v1/storage_backend/naive_serde/kivi_serde.py +22 -0
  369. lmcache/v1/storage_backend/naive_serde/naive_serde.py +18 -0
  370. lmcache/v1/storage_backend/naive_serde/serde.py +37 -0
  371. lmcache/v1/storage_backend/native_clients/connector_client_base.py +165 -0
  372. lmcache/v1/storage_backend/native_clients/resp_client.py +35 -0
  373. lmcache/v1/storage_backend/nixl_storage_backend.py +1400 -0
  374. lmcache/v1/storage_backend/p2p_backend.py +788 -0
  375. lmcache/v1/storage_backend/path_sharder.py +117 -0
  376. lmcache/v1/storage_backend/pd_backend.py +646 -0
  377. lmcache/v1/storage_backend/plugins/dax_backend.py +1443 -0
  378. lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +1361 -0
  379. lmcache/v1/storage_backend/remote_backend.py +624 -0
  380. lmcache/v1/storage_backend/resp_client.py +227 -0
  381. lmcache/v1/storage_backend/storage_backend_listener.py +19 -0
  382. lmcache/v1/storage_backend/storage_manager.py +1352 -0
  383. lmcache/v1/system_detection.py +110 -0
  384. lmcache/v1/token_database.py +551 -0
  385. lmcache/v1/transfer_channel/__init__.py +83 -0
  386. lmcache/v1/transfer_channel/abstract.py +285 -0
  387. lmcache/v1/transfer_channel/mock_memory_channel.py +156 -0
  388. lmcache/v1/transfer_channel/nixl_channel.py +639 -0
  389. lmcache/v1/transfer_channel/py_socket_channel.py +260 -0
  390. lmcache/v1/transfer_channel/transfer_utils.py +63 -0
  391. lmcache/v1/utils/__init__.py +1 -0
  392. lmcache/v1/utils/bloom_filter.py +109 -0
  393. lmcache/v1/utils/cache_utils.py +125 -0
  394. lmcache_cli-0.4.5.dev0.dist-info/METADATA +185 -0
  395. lmcache_cli-0.4.5.dev0.dist-info/RECORD +399 -0
  396. lmcache_cli-0.4.5.dev0.dist-info/WHEEL +5 -0
  397. lmcache_cli-0.4.5.dev0.dist-info/entry_points.txt +2 -0
  398. lmcache_cli-0.4.5.dev0.dist-info/licenses/LICENSE +201 -0
  399. lmcache_cli-0.4.5.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2619 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from collections import deque
4
+ from contextlib import nullcontext
5
+ from dataclasses import dataclass
6
+ from enum import Enum, auto
7
+ from functools import wraps
8
+ from typing import Any, List, Optional, Tuple, Union
9
+ import abc
10
+ import ctypes
11
+ import os
12
+ import threading
13
+
14
+ # Third Party
15
+ from sortedcontainers import SortedList
16
+ import torch
17
+
18
+ # First Party
19
+ from lmcache.integration.vllm.utils import get_size_bytes
20
+ from lmcache.logging import init_logger
21
+ from lmcache.observability import LMCStatsMonitor
22
+ from lmcache.utils import _lmcache_nvtx_annotate
23
+ from lmcache.v1.pin_monitor import PinMonitor
24
+ from lmcache.v1.system_detection import NUMAMapping
25
+ import lmcache.c_ops as lmc_ops
26
+
27
+ logger = init_logger(__name__)
28
+
29
+
30
+ # Helper functions for thread safety
31
+ def synchronized(lock_attr_name):
32
+ """
33
+ Decorator to make a method thread-safe by acquiring the lock
34
+ specified by lock_attr_name on the instance.
35
+ """
36
+
37
+ def decorator(method):
38
+ @wraps(method)
39
+ def wrapper(self, *args, **kwargs):
40
+ lock = getattr(self, lock_attr_name)
41
+ with lock:
42
+ return method(self, *args, **kwargs)
43
+
44
+ return wrapper
45
+
46
+ return decorator
47
+
48
+
49
+ class MemoryFormat(Enum):
50
+ UNDEFINED = 0
51
+ """[2, num_layers, num_tokens, hidden_dim]
52
+ """
53
+ # KV_BLOB = 1
54
+ KV_2LTD = auto()
55
+ """[num_tokens, 2, hidden_dim]
56
+ """
57
+ # LAYER_KV_BLOB = 2
58
+ KV_T2D = auto()
59
+ """[2, num_tokens, hidden_dim]
60
+ """
61
+
62
+ KV_2TD = auto()
63
+ """Compressed binary array format
64
+ """
65
+ BINARY = auto()
66
+
67
+ BINARY_BUFFER = auto()
68
+
69
+ KV_MLA_FMT = auto()
70
+ """[1, num_layers, num_tokens, aligned_head_size]
71
+ """
72
+
73
+ def token_dim(self) -> int:
74
+ if self == MemoryFormat.KV_2LTD:
75
+ return 2
76
+ elif self == MemoryFormat.KV_T2D:
77
+ return 1
78
+ elif self == MemoryFormat.KV_2TD:
79
+ return 0
80
+ elif self == MemoryFormat.BINARY:
81
+ return 0
82
+ elif self == MemoryFormat.BINARY_BUFFER:
83
+ return 0
84
+ elif self == MemoryFormat.KV_MLA_FMT:
85
+ return 2
86
+ return 0
87
+
88
+
89
+ @dataclass
90
+ class FreeBlock:
91
+ """Metadata class used by the memory allocators"""
92
+
93
+ start: int
94
+ size: int
95
+
96
+ def can_be_coalesced(self, succ: "FreeBlock") -> bool:
97
+ return self.start + self.size == succ.start
98
+
99
+
100
+ @dataclass
101
+ class MemoryObjMetadata:
102
+ # TODO(chunxiaozheng): use shapes and dtypes to replace shape and dtype
103
+ # The 'logical' shape of the tensor
104
+ shape: torch.Size
105
+
106
+ # The 'logical' dtype of the tensor
107
+ dtype: Optional[torch.dtype]
108
+
109
+ # The 'physical address' of the tensor
110
+ address: int
111
+
112
+ # The 'physical size' in bytes of the allocated memory
113
+ phy_size: int
114
+
115
+ # Reference count
116
+ ref_count: int
117
+
118
+ # Whether the object is pinned and cannot be evicted
119
+ # lookup pins are temporary
120
+ # cache controller pins are persistent
121
+ pin_count: int = 0
122
+
123
+ # The 'logical' format of the tensor
124
+ fmt: MemoryFormat = MemoryFormat.UNDEFINED
125
+
126
+ # Positions when the cache is stored
127
+ cached_positions: Optional[torch.Tensor] = None
128
+
129
+ # shapes and dtypes should be used in the future
130
+ shapes: Optional[list[torch.Size]] = None
131
+ dtypes: Optional[list[torch.dtype]] = None
132
+
133
+ def to_dict(self):
134
+ # Note(Kuntai): this is used for serializing MemoryObjMetadata via
135
+ # msgpack.
136
+ return {
137
+ "__type__": "MemoryObjMetadata",
138
+ "shape": list(self.shape), # torch.Size -> list
139
+ "dtype": str(self.dtype) if self.dtype else None,
140
+ "address": self.address,
141
+ "phy_size": self.phy_size,
142
+ "ref_count": self.ref_count,
143
+ "fmt": self.fmt.value,
144
+ "shapes": [list(shape) for shape in self.shapes] if self.shapes else None,
145
+ "dtypes": [str(dtype) for dtype in self.dtypes] if self.dtypes else None,
146
+ }
147
+
148
+ @staticmethod
149
+ def from_dict(d):
150
+ dtype_str = d["dtype"]
151
+ dtype = getattr(torch, dtype_str.replace("torch.", "")) if dtype_str else None
152
+ shapes_list = d["shapes"]
153
+ shapes = [torch.Size(s) for s in shapes_list] if shapes_list else None
154
+ dtypes_list = d["dtypes"]
155
+ dtypes = (
156
+ [getattr(torch, d_str.replace("torch.", "")) for d_str in dtypes_list]
157
+ if dtypes_list
158
+ else None
159
+ )
160
+ return MemoryObjMetadata(
161
+ shape=torch.Size(d["shape"]),
162
+ dtype=dtype,
163
+ address=d["address"],
164
+ phy_size=d["phy_size"],
165
+ ref_count=d["ref_count"],
166
+ fmt=MemoryFormat(d["fmt"]),
167
+ shapes=shapes,
168
+ dtypes=dtypes,
169
+ )
170
+
171
+ def get_size(self) -> int:
172
+ if self.shapes is not None and self.dtypes is not None:
173
+ return get_size_bytes(self.shapes, self.dtypes)
174
+ return self.shape.numel() * self.dtype.itemsize # type: ignore
175
+
176
+
177
+ class MemoryObj(metaclass=abc.ABCMeta):
178
+ """
179
+ MemoryObj interface.
180
+ """
181
+
182
+ # subclasses should expose raw_data differently
183
+ raw_data: Any
184
+
185
+ def __init__(self, metadata: MemoryObjMetadata):
186
+ self.meta = metadata
187
+
188
+ @abc.abstractmethod
189
+ def invalidate(self):
190
+ """
191
+ Invalidate the MemoryObj.
192
+ """
193
+ raise NotImplementedError
194
+
195
+ @abc.abstractmethod
196
+ def is_valid(self):
197
+ """
198
+ Check if the MemoryObj is valid.
199
+ """
200
+ raise NotImplementedError
201
+
202
+ @abc.abstractmethod
203
+ def get_size(self) -> int:
204
+ """
205
+ Get the size of the MemoryObj in bytes.
206
+ Note that this number could be smaller than the physical size.
207
+ The physical size is aligned to the allocator's alignment.
208
+ """
209
+ raise NotImplementedError
210
+
211
+ @abc.abstractmethod
212
+ def get_shape(self) -> torch.Size:
213
+ """
214
+ Get the shape of the MemoryObj.
215
+ """
216
+ raise NotImplementedError
217
+
218
+ def get_dtype(self) -> Optional[torch.dtype]:
219
+ """
220
+ Get the dtype of the MemoryObj.
221
+ """
222
+ return None
223
+
224
+ @abc.abstractmethod
225
+ def get_shapes(self) -> list[torch.Size]:
226
+ """
227
+ Get the shapes of the MemoryObj.
228
+ """
229
+ raise NotImplementedError
230
+
231
+ @abc.abstractmethod
232
+ def get_dtypes(self) -> list[torch.dtype]:
233
+ """
234
+ Get the dtypes of the MemoryObj.
235
+ """
236
+ raise NotImplementedError
237
+
238
+ @abc.abstractmethod
239
+ def get_memory_format(self) -> MemoryFormat:
240
+ """
241
+ Get the memory format of the MemoryObj.
242
+ """
243
+ raise NotImplementedError
244
+
245
+ @abc.abstractmethod
246
+ def get_physical_size(self) -> int:
247
+ """
248
+ Get the physical size of the MemoryObj in bytes.
249
+ """
250
+ raise NotImplementedError
251
+
252
+ @abc.abstractmethod
253
+ def pin(self) -> bool:
254
+ """
255
+ Pin the memory obj so that it will not be evicted.
256
+ """
257
+ raise NotImplementedError
258
+
259
+ @abc.abstractmethod
260
+ def ref_count_up(self):
261
+ """
262
+ Increase ref count for the given MemoryObj by one.
263
+ """
264
+ raise NotImplementedError
265
+
266
+ @abc.abstractmethod
267
+ def unpin(self) -> bool:
268
+ """
269
+ Unpin the memory obj so that it can be evicted.
270
+ """
271
+ raise NotImplementedError
272
+
273
+ @abc.abstractmethod
274
+ def ref_count_down(self):
275
+ """
276
+ Decrease ref count for the given MemoryObj by one.
277
+ """
278
+ raise NotImplementedError
279
+
280
+ @abc.abstractmethod
281
+ def get_ref_count(self) -> int:
282
+ """
283
+ Get ref count for the given MemoryObj.
284
+ """
285
+ raise NotImplementedError
286
+
287
+ @abc.abstractmethod
288
+ def get_num_tokens(self) -> int:
289
+ """
290
+ Get token number for the given MemoryObj.
291
+ """
292
+ raise NotImplementedError
293
+
294
+ @property
295
+ @abc.abstractmethod
296
+ def metadata(self) -> MemoryObjMetadata:
297
+ """
298
+ Get the metada of the MemoryObj.
299
+ """
300
+ raise NotImplementedError
301
+
302
+ @property
303
+ @abc.abstractmethod
304
+ def tensor(self) -> Optional[torch.Tensor]:
305
+ """
306
+ Get the tensor from the MemoryObj.
307
+ """
308
+ raise NotImplementedError
309
+
310
+ @property
311
+ @abc.abstractmethod
312
+ def byte_array(self) -> bytes:
313
+ """
314
+ Get the byte array from the MemoryObj.
315
+ The size is will be the physical size instead of the unaligned size.
316
+ """
317
+ raise NotImplementedError
318
+
319
+ @property
320
+ @abc.abstractmethod
321
+ def data_ptr(self) -> int:
322
+ """
323
+ Get the data pointer of the MemoryObj.
324
+ This is used to access the raw data in the memory.
325
+ """
326
+ raise NotImplementedError
327
+
328
+ @property
329
+ @abc.abstractmethod
330
+ def is_pinned(self) -> bool:
331
+ """
332
+ Check whether the memory obj is pinned.
333
+ """
334
+ raise NotImplementedError
335
+
336
+ @property
337
+ @abc.abstractmethod
338
+ def can_evict(self) -> bool:
339
+ """
340
+ Check whether the memory obj can be evicted.
341
+ """
342
+ raise NotImplementedError
343
+
344
+ @property
345
+ @abc.abstractmethod
346
+ def raw_tensor(self) -> Optional[torch.Tensor]:
347
+ """
348
+ Get the raw tensor from the MemoryObj.
349
+ """
350
+ raise NotImplementedError
351
+
352
+ @abc.abstractmethod
353
+ def get_tensor(self, index: int) -> Optional[torch.Tensor]:
354
+ """
355
+ Get the tensor from the MemoryObj at the given index(group).
356
+ """
357
+ raise NotImplementedError
358
+
359
+ @abc.abstractmethod
360
+ def parent(self) -> Optional["MemoryAllocatorInterface"]:
361
+ """
362
+ Get the allocator that allocates this memory object
363
+ """
364
+ raise NotImplementedError
365
+
366
+
367
+ def _resolve_pinned_alloc_free(
368
+ numa_mapping: Optional[NUMAMapping] = None,
369
+ shm_name: Optional[str] = None,
370
+ size: Optional[int] = None,
371
+ ) -> Tuple[
372
+ tuple, # (alloc_fn, *alloc_args)
373
+ tuple, # (free_fn, *free_args_after_ptr)
374
+ ]:
375
+ """Resolve the alloc/free function pair based on memory type.
376
+
377
+ Returns:
378
+ A tuple of (alloc_info, free_info) where:
379
+ - alloc_info: (alloc_fn, *args) to call as alloc_fn(size, *args)
380
+ - free_info: (free_fn, *args) to call as free_fn(ptr, *args)
381
+ """
382
+ if shm_name:
383
+ return (
384
+ (lmc_ops.alloc_shm_pinned_ptr, shm_name),
385
+ (lmc_ops.free_shm_pinned_ptr, size, shm_name),
386
+ )
387
+ elif numa_mapping:
388
+ if torch.cuda.is_available():
389
+ current_device_id = torch.cuda.current_device()
390
+ else:
391
+ current_device_id = 0
392
+ gpu_to_numa_mapping = numa_mapping.gpu_to_numa_mapping
393
+ assert current_device_id in gpu_to_numa_mapping, (
394
+ f"Current device {current_device_id} is not in the GPU NUMA mapping."
395
+ )
396
+ numa_id = gpu_to_numa_mapping[current_device_id]
397
+ return (
398
+ (lmc_ops.alloc_pinned_numa_ptr, numa_id),
399
+ (lmc_ops.free_pinned_numa_ptr, size),
400
+ )
401
+ else:
402
+ return (
403
+ (lmc_ops.alloc_pinned_ptr, 0),
404
+ (lmc_ops.free_pinned_ptr,),
405
+ )
406
+
407
+
408
+ def _allocate_cpu_memory(
409
+ size: int,
410
+ numa_mapping: Optional[NUMAMapping] = None,
411
+ shm_name: Optional[str] = None,
412
+ ) -> torch.Tensor:
413
+ if size == 0:
414
+ return torch.empty(0, dtype=torch.uint8)
415
+
416
+ alloc_info, _ = _resolve_pinned_alloc_free(
417
+ numa_mapping,
418
+ shm_name,
419
+ )
420
+ alloc_fn, *alloc_args = alloc_info
421
+ ptr = alloc_fn(size, *alloc_args)
422
+
423
+ array_type = ctypes.c_uint8 * size
424
+ buf = array_type.from_address(ptr)
425
+ buffer = torch.frombuffer(buf, dtype=torch.uint8)
426
+
427
+ return buffer
428
+
429
+
430
+ def _free_cpu_memory(
431
+ buffer: torch.Tensor,
432
+ size: int | None = None,
433
+ numa_mapping: Optional[NUMAMapping] = None,
434
+ shm_name: Optional[str] = None,
435
+ ) -> None:
436
+ if torch.cuda.is_available():
437
+ torch.cuda.synchronize()
438
+
439
+ _, free_info = _resolve_pinned_alloc_free(
440
+ numa_mapping,
441
+ shm_name,
442
+ size=size,
443
+ )
444
+ free_fn, *free_args = free_info
445
+ free_fn(buffer.data_ptr(), *free_args)
446
+
447
+
448
+ def _allocate_gpu_memory(
449
+ size: int,
450
+ device: str,
451
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
452
+ page_size = os.sysconf("SC_PAGESIZE")
453
+
454
+ # Over-allocate
455
+ base_buffer = torch.empty(size + page_size, dtype=torch.uint8, device=device)
456
+ offset = -base_buffer.data_ptr() % page_size
457
+
458
+ # Make aligned view
459
+ aligned_buffer = base_buffer[offset : offset + size]
460
+
461
+ # Need to return the base buffer as well in order to prevent GC
462
+ return base_buffer, aligned_buffer
463
+
464
+
465
+ class TensorMemoryObj(MemoryObj):
466
+ """
467
+ Wraps a raw flat tensor with some metadata
468
+ """
469
+
470
+ monitor = LMCStatsMonitor.GetOrCreate()
471
+
472
+ def __init__(
473
+ self,
474
+ raw_data: torch.Tensor,
475
+ metadata: MemoryObjMetadata,
476
+ parent_allocator: Optional["MemoryAllocatorInterface"],
477
+ ):
478
+ assert metadata.dtype is not None, "dtype must be specified for TensorMemoryObj"
479
+ super().__init__(metadata)
480
+ self.raw_data = raw_data
481
+ self.valid = True
482
+ self.lock = threading.Lock()
483
+ self.parent_allocator = parent_allocator
484
+ # Calculate the prefix sum of the group sizes
485
+ # If there are two groups, the prefix sum will be
486
+ # [0, size_of_group_1, size_of_group_1 + size_of_group_2]
487
+ self.group_prefix_sum = [0]
488
+ if self.meta.shapes is not None and self.meta.dtypes is not None:
489
+ size_in_bytes = 0
490
+ for shape, dtype in zip(self.meta.shapes, self.meta.dtypes, strict=True):
491
+ size_in_bytes += shape.numel() * dtype.itemsize
492
+ self.group_prefix_sum.append(size_in_bytes)
493
+ else:
494
+ self.group_prefix_sum.append(self.meta.get_size())
495
+
496
+ def __del__(self):
497
+ """
498
+ Destructor to ensure memory is released when the object is garbage collected.
499
+ This acts as a safety net to prevent memory leaks if ref_count_down() is not
500
+ called properly somewhere in the code path.
501
+ """
502
+ if self.parent_allocator is not None and self.is_valid():
503
+ if self.meta.ref_count > 0 or self.meta.pin_count > 0:
504
+ logger.warning(
505
+ "MemoryObj at %s is being garbage collected "
506
+ "with ref_count=%d, pin_count=%d. "
507
+ "This indicates ref_count_down()/unpin() was not called properly.",
508
+ self.meta.address,
509
+ self.meta.ref_count,
510
+ self.meta.pin_count,
511
+ )
512
+ self.parent_allocator.free(self)
513
+
514
+ def invalidate(self):
515
+ self.valid = False
516
+
517
+ def is_valid(self):
518
+ return self.valid
519
+
520
+ def get_size(self) -> int:
521
+ return self.group_prefix_sum[-1]
522
+
523
+ # TODO(chunxiaozheng): use get_shapes and get_dtypes to replace
524
+ # get_shape and get_dtype
525
+ def get_shape(self) -> torch.Size:
526
+ return self.meta.shape
527
+
528
+ def get_dtype(self) -> torch.dtype:
529
+ return self.meta.dtype
530
+
531
+ def get_shapes(self) -> list[torch.Size]:
532
+ assert self.meta.shapes is not None
533
+ return self.meta.shapes
534
+
535
+ def get_dtypes(self) -> list[torch.dtype]:
536
+ assert self.meta.dtypes is not None
537
+ return self.meta.dtypes
538
+
539
+ def get_memory_format(self) -> MemoryFormat:
540
+ with self.lock:
541
+ return self.meta.fmt
542
+
543
+ def get_physical_size(self) -> int:
544
+ return self.meta.phy_size
545
+
546
+ def ref_count_up(self):
547
+ with self.lock:
548
+ self.meta.ref_count += 1
549
+
550
+ def ref_count_down(self):
551
+ with self.lock:
552
+ self.meta.ref_count -= 1
553
+ if self.meta.ref_count < 0:
554
+ logger.warning(
555
+ f"Ref count of MemoryObj {self.meta.address}"
556
+ f"is negative: {self.meta.ref_count}."
557
+ "Double free occurred somewhere."
558
+ "Setting ref count back to 0 as a hack but please find the bug."
559
+ )
560
+ self.meta.ref_count = 0
561
+ if (
562
+ self.meta.ref_count == 0
563
+ and self.parent_allocator is not None
564
+ and self.meta.pin_count == 0
565
+ ):
566
+ self.parent_allocator.free(self)
567
+
568
+ def get_ref_count(self) -> int:
569
+ with self.lock:
570
+ return self.meta.ref_count
571
+
572
+ def get_num_tokens(self) -> int:
573
+ with self.lock:
574
+ token_dim = self.meta.fmt.token_dim()
575
+ return self.meta.shape[token_dim]
576
+
577
+ def pin(self) -> bool:
578
+ with self.lock:
579
+ # if pin_count is 0, indicates that the object is pinned for the first time
580
+ if self.meta.pin_count == 0:
581
+ TensorMemoryObj.monitor.update_pinned_memory_objs_count(1)
582
+
583
+ self.meta.pin_count += 1
584
+
585
+ # Register/update with PinMonitor for timeout tracking on every pin
586
+ pin_monitor = PinMonitor.GetOrCreate()
587
+ pin_monitor.on_pin(self)
588
+ return True
589
+
590
+ def unpin(self) -> bool:
591
+ with self.lock:
592
+ self.meta.pin_count -= 1
593
+
594
+ # if pin_count is 0, indicates that the object is unpinned
595
+ if self.meta.pin_count == 0:
596
+ TensorMemoryObj.monitor.update_pinned_memory_objs_count(-1)
597
+ # Unregister from PinMonitor when fully unpinned
598
+ pin_monitor = PinMonitor.GetOrCreate()
599
+ pin_monitor.on_unpin(self)
600
+
601
+ if self.meta.pin_count <= 0 and self.meta.ref_count <= 0:
602
+ if self.parent_allocator is None:
603
+ logger.error(
604
+ "Parent allocator is None when trying to free MemoryObj."
605
+ "This could cause memory leak"
606
+ )
607
+ else:
608
+ self.parent_allocator.free(self)
609
+
610
+ if self.meta.pin_count < 0:
611
+ logger.warning(
612
+ f"Pin count of MemoryObj {self.meta.address}"
613
+ f"is negative: {self.meta.pin_count}."
614
+ "Double unpin occurred somewhere."
615
+ "Setting pin count back to 0 as a hack but please find the bug."
616
+ )
617
+ self.meta.pin_count = 0
618
+ return True
619
+
620
+ @property
621
+ def metadata(self) -> MemoryObjMetadata:
622
+ with self.lock:
623
+ return self.meta
624
+
625
+ @property
626
+ def tensor(self) -> Optional[torch.Tensor]:
627
+ if not self.valid:
628
+ logger.warning("Trying to access an invalidated MemoryObj")
629
+ return None
630
+ assert self.meta.dtype is not None
631
+ # TODO(Jiayi): consider caching the `get_size()`
632
+ return (
633
+ self.raw_data[: self.get_size()].view(self.meta.dtype).view(self.meta.shape)
634
+ )
635
+
636
+ @property
637
+ def byte_array(self) -> memoryview:
638
+ # TODO: consider using one of the alternatives
639
+
640
+ # Alternative 1:
641
+ # # PyTorch tensors support buffer protocol directly for CPU tensors
642
+ # return memoryview(self.raw_data)
643
+
644
+ # Alternative 2:
645
+ # assert self.raw_data.device.type == 'cpu',
646
+ # "byte_array only works with CPU tensors"
647
+ # return memoryview(self.raw_data.contiguous().numpy())
648
+
649
+ # Use logical size (get_size) rather than raw_data physical size.
650
+ # The raw_data buffer may include alignment padding (e.g. from
651
+ # batched_allocate) that must not be exposed to callers such as
652
+ # remote-backend put/get which rely on byte_array length matching
653
+ # the metadata length.
654
+ num_bytes = self.get_size()
655
+ ptr = self.raw_data.data_ptr()
656
+ ubyte_ptr = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_ubyte))
657
+ byte_array = (ctypes.c_ubyte * num_bytes).from_address(
658
+ ctypes.addressof(ubyte_ptr.contents)
659
+ )
660
+ return memoryview(byte_array)
661
+
662
+ @property
663
+ def data_ptr(self) -> int:
664
+ return self.raw_data.data_ptr()
665
+
666
+ @property
667
+ def is_pinned(self) -> bool:
668
+ return self.metadata.pin_count > 0
669
+
670
+ @property
671
+ def can_evict(self) -> bool:
672
+ """
673
+ Check whether the memory obj can be evicted.
674
+ A memory obj can be evicted if it is not pinned and ref_count=1.
675
+ """
676
+ return not self.is_pinned and self.get_ref_count() == 1
677
+
678
+ @property
679
+ def raw_tensor(self) -> Optional[torch.Tensor]:
680
+ if not self.valid:
681
+ logger.warning("Trying to access an invalidated MemoryObj")
682
+ return None
683
+ return self.raw_data
684
+
685
+ def get_tensor(self, index: int) -> Optional[torch.Tensor]:
686
+ if not self.valid:
687
+ logger.warning("Trying to access an invalidated MemoryObj")
688
+ return None
689
+ assert self.meta.shapes is not None
690
+ assert self.meta.dtypes is not None
691
+ begin = self.group_prefix_sum[index]
692
+ end = self.group_prefix_sum[index + 1]
693
+ return (
694
+ self.raw_data[begin:end]
695
+ .view(self.meta.dtypes[index])
696
+ .view(self.meta.shapes[index])
697
+ )
698
+
699
+ def parent(self) -> Optional["MemoryAllocatorInterface"]:
700
+ return self.parent_allocator
701
+
702
+
703
+ class BytesBufferMemoryObj(MemoryObj):
704
+ """
705
+ Wraps a raw flat tensor with some metadata
706
+ """
707
+
708
+ def __init__(self, raw_bytes: bytes, metadata: Optional[MemoryObjMetadata] = None):
709
+ self.raw_data = raw_bytes
710
+ if metadata is None:
711
+ bytes_shape = torch.Size([len(self.raw_data), 0, 0, 0])
712
+ metadata = MemoryObjMetadata(
713
+ shape=bytes_shape,
714
+ dtype=None,
715
+ address=0,
716
+ phy_size=0,
717
+ ref_count=1,
718
+ pin_count=0,
719
+ fmt=MemoryFormat.BINARY_BUFFER,
720
+ )
721
+ super().__init__(metadata)
722
+ self.valid = True
723
+
724
+ def invalidate(self):
725
+ self.valid = False
726
+
727
+ def is_valid(self):
728
+ return self.valid
729
+
730
+ def get_size(self) -> int:
731
+ return len(self.raw_data)
732
+
733
+ def get_shape(self) -> torch.Size:
734
+ return torch.Size([len(self.raw_data), 0, 0, 0])
735
+
736
+ def get_dtype(self) -> Optional[torch.dtype]:
737
+ return None
738
+
739
+ def get_shapes(self) -> list[torch.Size]:
740
+ return [self.get_shape()]
741
+
742
+ def get_dtypes(self) -> list[torch.dtype]:
743
+ return []
744
+
745
+ def get_memory_format(self) -> MemoryFormat:
746
+ return self.metadata.fmt
747
+
748
+ def get_physical_size(self) -> int:
749
+ return self.metadata.phy_size
750
+
751
+ def pin(self) -> bool:
752
+ self.metadata.pin_count += 1
753
+ return True
754
+
755
+ def unpin(self) -> bool:
756
+ self.metadata.pin_count -= 1
757
+ if self.metadata.pin_count < 0:
758
+ logger.warning(
759
+ f"Pin count of MemoryObj {self.meta.address}"
760
+ f"is negative: {self.meta.pin_count}."
761
+ "Double unpin occurred somewhere."
762
+ "Setting pin count back to 0 as a hack but please find the bug."
763
+ )
764
+ self.metadata.pin_count = 0
765
+ return True
766
+
767
+ def ref_count_up(self):
768
+ pass
769
+
770
+ def ref_count_down(self):
771
+ pass
772
+
773
+ def get_ref_count(self) -> int:
774
+ return 1
775
+
776
+ def get_num_tokens(self) -> int:
777
+ # TODO(Jiayi): record the number of tokens somehow
778
+ return 1
779
+
780
+ @property
781
+ def metadata(self) -> MemoryObjMetadata:
782
+ return self.meta
783
+
784
+ @property
785
+ def tensor(self) -> Optional[torch.Tensor]:
786
+ if not self.valid:
787
+ logger.warning("Trying to access an invalidated MemoryObj")
788
+ return None
789
+ return None
790
+
791
+ @property
792
+ def byte_array(self) -> bytes:
793
+ return self.raw_data
794
+
795
+ @property
796
+ def data_ptr(self) -> int:
797
+ mv = memoryview(self.raw_data)
798
+ addr = ctypes.addressof(ctypes.c_char.from_buffer(mv))
799
+ return addr
800
+
801
+ @property
802
+ def is_pinned(self) -> bool:
803
+ return self.metadata.pin_count > 0
804
+
805
+ @property
806
+ def can_evict(self) -> bool:
807
+ """
808
+ Check whether the memory obj can be evicted.
809
+ A buffer memory obj can be evicted if it is not pinned.
810
+ """
811
+ return not self.is_pinned
812
+
813
+ @property
814
+ def raw_tensor(self) -> Optional[torch.Tensor]:
815
+ if not self.valid:
816
+ logger.warning("Trying to access an invalidated MemoryObj")
817
+ return None
818
+ return None
819
+
820
+ def get_tensor(self, index: int) -> Optional[torch.Tensor]:
821
+ return None
822
+
823
+ def parent(self) -> Optional["MemoryAllocatorInterface"]:
824
+ # NOTE: BytesBufferMemoryObj may not be allocated by any allocator,
825
+ # so just return None here
826
+ return None
827
+
828
+
829
+ class MemoryAllocatorInterface(metaclass=abc.ABCMeta):
830
+ @abc.abstractmethod
831
+ def allocate(
832
+ self,
833
+ shapes: Union[torch.Size, list[torch.Size]],
834
+ dtypes: Union[torch.dtype, list[torch.dtype]],
835
+ fmt: MemoryFormat = MemoryFormat.UNDEFINED,
836
+ allocator_type: Optional[str] = None,
837
+ ) -> Optional[MemoryObj]:
838
+ """
839
+ Allocates the memory to hold a tensor of the given shape.
840
+
841
+ :param torch.Size shapes: The shape of the tensor to allocate.
842
+ :param torch.dtype dtypes: The dtype of the tensor to allocate.
843
+ :param MemoryFormat fmt: The format of the memory to allocate.
844
+
845
+ :return: A MemoryObj wrapping the allocated memory. Returns
846
+ None if the allocation failed.
847
+
848
+ :rtype: Optional[MemoryObj]
849
+ """
850
+ raise NotImplementedError
851
+
852
+ @abc.abstractmethod
853
+ def batched_allocate(
854
+ self,
855
+ shapes: Union[torch.Size, list[torch.Size]],
856
+ dtypes: Union[torch.dtype, list[torch.dtype]],
857
+ batch_size: int,
858
+ fmt: MemoryFormat = MemoryFormat.UNDEFINED,
859
+ allocator_type: Optional[str] = None,
860
+ ) -> Optional[List[MemoryObj]]:
861
+ """
862
+ Batched allocate the memory to hold a tensor of the given shape.
863
+
864
+ :param torch.Size shapes: The shape of the tensor to allocate.
865
+ :param torch.dtype dtypes: The dtype of the tensor to allocate.
866
+ :param int batch_size: The number of tensors to allocate.
867
+ :param MemoryFormat fmt: The format of the memory to allocate.
868
+
869
+ :return: A list of MemoryObjs wrapping the allocated memory.
870
+ Returns None if the allocation failed.
871
+
872
+ :rtype: Optional[List[MemoryObj]]
873
+ """
874
+ raise NotImplementedError
875
+
876
+ @abc.abstractmethod
877
+ def free(
878
+ self,
879
+ memory_obj: MemoryObj,
880
+ allocator_type: Optional[str] = None,
881
+ ):
882
+ """
883
+ Frees the memory allocated for the given MemoryObj.
884
+ Note that this function shouldn't be explicitly called.
885
+ Instead, use `ref_count_down` to decrease ref count.
886
+
887
+ :param MemoryObj memory_obj: The MemoryObj to free.
888
+ """
889
+ raise NotImplementedError
890
+
891
+ @abc.abstractmethod
892
+ def batched_free(
893
+ self,
894
+ memory_objs: List[MemoryObj],
895
+ allocator_type: Optional[str] = None,
896
+ update_stats: bool = True,
897
+ ):
898
+ """
899
+ Frees the memory allocated for the given list of MemoryObjs.
900
+
901
+ :param List[MemoryObj] memory_objs: The list of MemoryObjs
902
+ to free.
903
+ """
904
+ raise NotImplementedError
905
+
906
+ def close(self):
907
+ """
908
+ Closes the memory allocator.
909
+ This is called when the LMCacheEngine is closed.
910
+ """
911
+ return
912
+
913
+ def memcheck(self) -> bool:
914
+ """
915
+ Checks the memory allocator for consistency.
916
+
917
+ Returns:
918
+ True if everything is fine otherwise False
919
+ """
920
+ return True
921
+
922
+ # TODO(chunxiaozheng): remove if after all params replaced by shapes/dtypes
923
+ def _adapt_shapes_and_dtypes(
924
+ self,
925
+ shapes: Union[torch.Size, list[torch.Size]],
926
+ dtypes: Union[torch.dtype, list[torch.dtype]],
927
+ ) -> Tuple[list[torch.Size], list[torch.dtype]]:
928
+ if isinstance(shapes, torch.Size):
929
+ shapes = [shapes]
930
+
931
+ if isinstance(dtypes, torch.dtype):
932
+ dtypes = [dtypes]
933
+
934
+ assert len(shapes) == len(dtypes), (
935
+ f"shapes and dtypes must have the same length, "
936
+ f"got {len(shapes)} and {len(dtypes)}, "
937
+ f"shapes: {shapes}, dtypes: {dtypes}"
938
+ )
939
+ return shapes, dtypes
940
+
941
+
942
+ class AddressManager:
943
+ """
944
+ Manages a virtual address space starting from 0 for memory allocation.
945
+
946
+ Key interfaces:
947
+ - allocate(size): Allocate a block of memory of the given size. The starting
948
+ address and the actual allocated size will be aligned.
949
+
950
+ - free(address, size): Free a previously allocated region. Note that if the
951
+ region is not "allocated" before, it may have internal errors.
952
+
953
+ - sbrk(size): Expand the virtual address space by the given size. The size
954
+ will be aligned internally.
955
+
956
+ Core assumptions:
957
+ - The allocated size should be aligned with ALIGN_BYTES.
958
+ """
959
+
960
+ ALIGN_BYTES = 4096
961
+
962
+ def __init__(self, size: int, align_bytes: int = ALIGN_BYTES):
963
+ """
964
+ Initializes the AddressManager with a given size.
965
+
966
+ Args:
967
+ size: The initial size of the virtual address space.
968
+ align_bytes: The alignment requirement for allocations.
969
+ """
970
+ self._size = size
971
+ self._align = align_bytes
972
+
973
+ # Current implementation: explicit list
974
+ self._explicit_list: SortedList[FreeBlock] = SortedList(key=lambda x: x.start)
975
+ self._explicit_list.add(FreeBlock(start=0, size=size))
976
+
977
+ # thread safe lock
978
+ self._lock = threading.Lock()
979
+
980
+ # For debugging purposes
981
+ self.total_allocated_size = 0
982
+
983
+ def compute_aligned_size(self, raw_size: int) -> int:
984
+ """
985
+ Helper function to compute the aligned size for a given raw size.
986
+
987
+ Args:
988
+ raw_size: The raw size to be aligned.
989
+
990
+ Returns:
991
+ The aligned size.
992
+ """
993
+ return (raw_size + self._align - 1) & ~(self._align - 1)
994
+
995
+ def _can_merge_with_prev(
996
+ self, curr_block: FreeBlock, prev_block: FreeBlock
997
+ ) -> bool:
998
+ """Hook: Check if curr_block can merge with prev_block."""
999
+ return prev_block.can_be_coalesced(curr_block)
1000
+
1001
+ def _can_merge_with_succ(
1002
+ self, curr_block: FreeBlock, succ_block: FreeBlock
1003
+ ) -> bool:
1004
+ """Hook: Check if curr_block can merge with succ_block."""
1005
+ return curr_block.can_be_coalesced(succ_block)
1006
+
1007
+ @_lmcache_nvtx_annotate
1008
+ def _coalesce(
1009
+ self,
1010
+ curr_block: FreeBlock,
1011
+ prev_block: Optional[FreeBlock],
1012
+ succ_block: Optional[FreeBlock],
1013
+ ):
1014
+ """
1015
+ Coalesces the current block with the previous and/or successor block.
1016
+ This assumes the curr_block is NOT in self._explicit_list
1017
+
1018
+ Returns True if the current block was coalesced, otherwise False.
1019
+ """
1020
+ merge_prev = prev_block is not None and self._can_merge_with_prev(
1021
+ curr_block, prev_block
1022
+ )
1023
+ merge_succ = succ_block is not None and self._can_merge_with_succ(
1024
+ curr_block, succ_block
1025
+ )
1026
+
1027
+ if merge_prev and merge_succ:
1028
+ prev_block.size += curr_block.size + succ_block.size # type: ignore
1029
+ self._explicit_list.remove(succ_block)
1030
+ elif merge_prev:
1031
+ prev_block.size += curr_block.size # type: ignore
1032
+ elif merge_succ:
1033
+ # NOTE: logically, this won't change the order of the succ_block,
1034
+ # so we don't need to do a "remove" and "reinsert" here
1035
+ self._explicit_list.remove(succ_block)
1036
+ succ_block.start -= curr_block.size # type: ignore
1037
+ succ_block.size += curr_block.size # type: ignore
1038
+ self._explicit_list.add(succ_block)
1039
+
1040
+ return merge_prev or merge_succ
1041
+
1042
+ @_lmcache_nvtx_annotate
1043
+ @synchronized("_lock")
1044
+ def allocate(self, size: int) -> tuple[int, int]:
1045
+ """
1046
+ Allocate a block of memory from the virtual address space of a given
1047
+ size. The actual allocated size could be larger than the requested size
1048
+ in order to satisfy alignment requirements.
1049
+
1050
+ Args:
1051
+ size: The requested size of the memory block. Should be greater
1052
+ than 0.
1053
+
1054
+ Returns:
1055
+ A tuple (address, allocated_size) where address is the starting
1056
+ address of the allocated block and allocated_size is the actual
1057
+ size of the allocated block.
1058
+
1059
+ Raises:
1060
+ RuntimeError: If no memory is available to allocate.
1061
+ """
1062
+ aligned_size = self.compute_aligned_size(size)
1063
+ for block in self._explicit_list:
1064
+ if block.size >= aligned_size:
1065
+ break
1066
+ else:
1067
+ logger.warning(
1068
+ "Failed to allocate memory block of size %d "
1069
+ "because no memory is available",
1070
+ size,
1071
+ )
1072
+ raise RuntimeError(
1073
+ f"Failed to allocate memory block of size {size} "
1074
+ "because no memory is available"
1075
+ )
1076
+
1077
+ self._explicit_list.remove(block)
1078
+ if block.size > aligned_size:
1079
+ self._explicit_list.add(
1080
+ FreeBlock(
1081
+ start=block.start + aligned_size,
1082
+ size=block.size - aligned_size,
1083
+ )
1084
+ )
1085
+
1086
+ # For debug
1087
+ self.total_allocated_size += aligned_size
1088
+
1089
+ return block.start, aligned_size
1090
+
1091
+ @_lmcache_nvtx_annotate
1092
+ @synchronized("_lock")
1093
+ def batched_allocate(self, size: int, batch_size: int) -> list[tuple[int, int]]:
1094
+ """
1095
+ Allocate blocks of memory from the virtual address space of a given
1096
+ size and batch size. The actual allocated size could be larger than
1097
+ the requested size in order to satisfy alignment requirements.
1098
+
1099
+ Args:
1100
+ size: The requested size of the memory block. Should be greater
1101
+ than 0.
1102
+ batch_size: The number of memory blocks to allocate.
1103
+
1104
+ Returns:
1105
+ A list of tuple (address, allocated_size) where address is the starting
1106
+ address of the allocated block and allocated_size is the actual size of
1107
+ the allocated block.
1108
+ Note: the length of the return list is the same as the batch_size.
1109
+
1110
+ Raises:
1111
+ RuntimeError: If no memory is available to allocate.
1112
+ """
1113
+ aligned_size = self.compute_aligned_size(size)
1114
+ remaining = batch_size
1115
+ allocate_result: list[tuple[int, int]] = []
1116
+
1117
+ blocks_to_remove: list[FreeBlock] = []
1118
+ blocks_to_add: list[FreeBlock] = []
1119
+
1120
+ for block in self._explicit_list:
1121
+ if remaining <= 0:
1122
+ break
1123
+ if block.size < aligned_size:
1124
+ continue
1125
+
1126
+ # Greedily carve out as many aligned_size chunks as possible
1127
+ num_from_block = min(remaining, block.size // aligned_size)
1128
+ start = block.start
1129
+ for i in range(num_from_block):
1130
+ allocate_result.append((start + i * aligned_size, aligned_size))
1131
+ remaining -= num_from_block
1132
+
1133
+ # Mark the original block for removal
1134
+ blocks_to_remove.append(block)
1135
+
1136
+ # Keep the remaining tail as a new free block if any space is left
1137
+ used = num_from_block * aligned_size
1138
+ if block.size > used:
1139
+ blocks_to_add.append(
1140
+ FreeBlock(start=block.start + used, size=block.size - used)
1141
+ )
1142
+
1143
+ if remaining > 0:
1144
+ # Not enough memory; free list is untouched, no rollback needed
1145
+ logger.warning(
1146
+ "Failed to batched allocate %d memory blocks of size %d "
1147
+ "because no enough memory is available (short by %d blocks)",
1148
+ batch_size,
1149
+ size,
1150
+ remaining,
1151
+ )
1152
+ raise RuntimeError(
1153
+ f"Failed to batched allocate {batch_size} memory blocks "
1154
+ f"of size {size} because no enough memory is available"
1155
+ )
1156
+ if len(allocate_result) != batch_size:
1157
+ # The length of allocate_result is not equal to batch_size;
1158
+ # free list is untouched, no rollback needed
1159
+ logger.warning(
1160
+ "Failed to batched allocate %d memory blocks of size %d "
1161
+ "because the length of allocate_result %d is not equal to batch_size",
1162
+ batch_size,
1163
+ size,
1164
+ len(allocate_result),
1165
+ )
1166
+ raise RuntimeError(
1167
+ f"Failed to batched allocate {batch_size} memory blocks "
1168
+ f"of size {size} because the length of allocate_result "
1169
+ f"{len(allocate_result)} is not equal to batch_size"
1170
+ )
1171
+
1172
+ # Allocation succeeded; batch-update the free list
1173
+ for block in blocks_to_remove:
1174
+ self._explicit_list.remove(block)
1175
+ for block in blocks_to_add:
1176
+ self._explicit_list.add(block)
1177
+
1178
+ # Update debug statistics
1179
+ total_allocated = aligned_size * batch_size
1180
+ self.total_allocated_size += total_allocated
1181
+
1182
+ return allocate_result
1183
+
1184
+ @_lmcache_nvtx_annotate
1185
+ @synchronized("_lock")
1186
+ def free(self, address: int, size: int):
1187
+ """
1188
+ Free a previously allocated block of memory.
1189
+
1190
+ Args:
1191
+ address: The starting address of the block to free.
1192
+ size: The size of the block to free. Should be greater than 0.
1193
+ """
1194
+ new_free_block = FreeBlock(start=address, size=size)
1195
+ index = self._explicit_list.bisect_left(new_free_block)
1196
+ prev_block = self._explicit_list[index - 1] if index > 0 else None
1197
+ succ_block = (
1198
+ self._explicit_list[index] if index < len(self._explicit_list) else None
1199
+ )
1200
+
1201
+ coalesced = self._coalesce(new_free_block, prev_block, succ_block)
1202
+ if not coalesced:
1203
+ self._explicit_list.add(new_free_block)
1204
+
1205
+ # For debug
1206
+ self.total_allocated_size -= size
1207
+
1208
+ @synchronized("_lock")
1209
+ def sbrk(self, size: int):
1210
+ """
1211
+ Expand the virtual address space by a given size.
1212
+
1213
+ Args:
1214
+ size: The size to expand the address space. Will be aligned internally
1215
+ with the ALIGN_BYTES
1216
+ """
1217
+ size = self.compute_aligned_size(size)
1218
+ new_block = FreeBlock(start=self._size, size=size)
1219
+ prev_block = self._explicit_list[-1] if len(self._explicit_list) > 0 else None
1220
+ succ_block = None
1221
+ coalesced = self._coalesce(new_block, prev_block, succ_block)
1222
+ if not coalesced:
1223
+ self._explicit_list.add(new_block)
1224
+
1225
+ self._size += size
1226
+
1227
+ def get_heap_size(self) -> int:
1228
+ """
1229
+ Get the total size of the address space.
1230
+
1231
+ Returns:
1232
+ The total size in bytes.
1233
+ """
1234
+ return self._size
1235
+
1236
+ def get_free_size(self) -> int:
1237
+ """
1238
+ Get the total free size in the address space.
1239
+
1240
+ Returns:
1241
+ The total free size in bytes.
1242
+ """
1243
+ return self._size - self.total_allocated_size
1244
+
1245
+ def check_consistency(self) -> bool:
1246
+ """
1247
+ Check if the address manager is consistent.
1248
+
1249
+ Returns:
1250
+ True if consistent, False otherwise.
1251
+ """
1252
+ # Check if free blocks are properly coalesced
1253
+ for prev, succ in zip(
1254
+ self._explicit_list[:-1], self._explicit_list[1:], strict=False
1255
+ ):
1256
+ if prev.can_be_coalesced(succ):
1257
+ return False
1258
+
1259
+ # Check if total size matches
1260
+ total_free_size = sum(block.size for block in self._explicit_list)
1261
+ if total_free_size + self.total_allocated_size != self._size:
1262
+ return False
1263
+
1264
+ return True
1265
+
1266
+
1267
+ class TensorMemoryAllocator(MemoryAllocatorInterface):
1268
+ """
1269
+ Implements a "explicit list" memory allocator.
1270
+ Uses AddressManager for address space management.
1271
+ """
1272
+
1273
+ def __init__(
1274
+ self,
1275
+ tensor: torch.Tensor,
1276
+ align_bytes: int = AddressManager.ALIGN_BYTES,
1277
+ init_address_space: int | None = None,
1278
+ ):
1279
+ """
1280
+ Args:
1281
+ tensor: The pre-allocated flat tensor to use as the memory pool.
1282
+ align_bytes: The alignment requirement for allocations.
1283
+ init_address_space: Initial size of the address space. If None,
1284
+ use the size of the provided tensor.
1285
+
1286
+ Note:
1287
+ The `init_address_space` is used for lazy memory allocation.
1288
+ We probably want to have a better way to make sure that the
1289
+ LazyMemoryAllocator can be decoupled from TensorMemoryAllocator.
1290
+ """
1291
+ self.buffer = tensor.view(torch.uint8).flatten()
1292
+
1293
+ # Use AddressManager for address space management
1294
+ self.address_manager = AddressManager(
1295
+ self.buffer.numel() if init_address_space is None else init_address_space,
1296
+ align_bytes,
1297
+ )
1298
+
1299
+ # For debugging purposes
1300
+ self.num_active_allocations = 0
1301
+
1302
+ self.stats_monitor = LMCStatsMonitor.GetOrCreate()
1303
+
1304
+ @property
1305
+ def total_allocated_size(self) -> int:
1306
+ return self.address_manager.total_allocated_size
1307
+
1308
+ @_lmcache_nvtx_annotate
1309
+ def allocate(
1310
+ self,
1311
+ shapes: Union[torch.Size, list[torch.Size]],
1312
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1313
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1314
+ allocator_type: Optional[str] = None,
1315
+ ) -> Optional[TensorMemoryObj]:
1316
+ shapes, dtypes = self._adapt_shapes_and_dtypes(shapes, dtypes)
1317
+
1318
+ # Calculate the size of the tensor
1319
+ raw_size = get_size_bytes(shapes, dtypes)
1320
+
1321
+ # Allocate from address manager
1322
+ try:
1323
+ block_start, aligned_size = self.address_manager.allocate(raw_size)
1324
+ except RuntimeError:
1325
+ # No block found
1326
+ return None
1327
+
1328
+ # For debug
1329
+ self.num_active_allocations += 1
1330
+
1331
+ # Update stats
1332
+ self.stats_monitor.update_local_cache_usage(
1333
+ self.address_manager.total_allocated_size
1334
+ )
1335
+ self.stats_monitor.update_active_memory_objs_count(self.num_active_allocations)
1336
+
1337
+ # Allocate the block
1338
+ raw_data = self._get_buffer_slice(block_start, raw_size)
1339
+ return TensorMemoryObj(
1340
+ raw_data=raw_data,
1341
+ metadata=MemoryObjMetadata(
1342
+ shapes[0],
1343
+ dtypes[0],
1344
+ block_start,
1345
+ aligned_size,
1346
+ 1,
1347
+ 0,
1348
+ fmt,
1349
+ shapes=shapes,
1350
+ dtypes=dtypes,
1351
+ ),
1352
+ parent_allocator=self,
1353
+ )
1354
+
1355
+ def _get_buffer_slice(self, start: int, size: int) -> torch.Tensor:
1356
+ """Hook: Get buffer slice. Override for custom buffer access."""
1357
+ return self.buffer[start : start + size]
1358
+
1359
+ @_lmcache_nvtx_annotate
1360
+ def batched_allocate(
1361
+ self,
1362
+ shapes: Union[torch.Size, list[torch.Size]],
1363
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1364
+ batch_size: int,
1365
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1366
+ allocator_type: Optional[str] = None,
1367
+ ) -> Optional[List[TensorMemoryObj]]:
1368
+ """
1369
+ Batched allocate tensor memory objs with equal sizes.
1370
+ """
1371
+ shapes, dtypes = self._adapt_shapes_and_dtypes(shapes, dtypes)
1372
+
1373
+ # Calculate the size of the tensor
1374
+ unit_raw_size = get_size_bytes(shapes, dtypes)
1375
+ unit_aligned_size = self.address_manager.compute_aligned_size(unit_raw_size)
1376
+
1377
+ try:
1378
+ alloc_results = self.address_manager.batched_allocate(
1379
+ unit_aligned_size, batch_size
1380
+ )
1381
+ except RuntimeError:
1382
+ return None
1383
+ addresses = [addr for addr, _ in alloc_results]
1384
+ raw_datas = [
1385
+ self._get_buffer_slice(addr, unit_aligned_size) for addr in addresses
1386
+ ]
1387
+
1388
+ # For debug
1389
+ self.num_active_allocations += batch_size
1390
+
1391
+ # Update stats
1392
+ self.stats_monitor.update_local_cache_usage(
1393
+ self.address_manager.total_allocated_size
1394
+ )
1395
+ self.stats_monitor.update_active_memory_objs_count(self.num_active_allocations)
1396
+
1397
+ tensor_mem_objs = []
1398
+ for raw_data, address in zip(raw_datas, addresses, strict=True):
1399
+ tensor_mem_objs.append(
1400
+ TensorMemoryObj(
1401
+ raw_data=raw_data,
1402
+ metadata=MemoryObjMetadata(
1403
+ shapes[0],
1404
+ dtypes[0],
1405
+ address,
1406
+ unit_aligned_size,
1407
+ 1,
1408
+ 0,
1409
+ fmt,
1410
+ shapes=shapes,
1411
+ dtypes=dtypes,
1412
+ ),
1413
+ parent_allocator=self,
1414
+ )
1415
+ )
1416
+
1417
+ return tensor_mem_objs
1418
+
1419
+ @_lmcache_nvtx_annotate
1420
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
1421
+ if not memory_obj.is_valid():
1422
+ return
1423
+
1424
+ self.address_manager.free(memory_obj.meta.address, memory_obj.meta.phy_size)
1425
+ memory_obj.invalidate()
1426
+
1427
+ # For debug
1428
+ self.num_active_allocations -= 1
1429
+
1430
+ # Update stats
1431
+ self.stats_monitor.update_local_cache_usage(
1432
+ self.address_manager.total_allocated_size
1433
+ )
1434
+ self.stats_monitor.update_active_memory_objs_count(self.num_active_allocations)
1435
+
1436
+ @_lmcache_nvtx_annotate
1437
+ def batched_free(
1438
+ self,
1439
+ memory_objs: List[MemoryObj],
1440
+ allocator_type: Optional[str] = None,
1441
+ update_stats: bool = True,
1442
+ ):
1443
+ """
1444
+ Batched free memory objs.
1445
+ Unlike `batched_allocate`, this function does not
1446
+ assume that the memory objs are equal-sized.
1447
+ """
1448
+ if not memory_objs:
1449
+ return
1450
+
1451
+ # Coalesce adjacent memory objects before freeing to reduce
1452
+ # the number of free operations
1453
+ coalesced_blocks: list[tuple[int, int, int]] = [] # (address, size, count)
1454
+ curr_start = None
1455
+ curr_size = 0
1456
+ curr_count = 0
1457
+
1458
+ memory_objs.sort(key=lambda x: x.meta.address)
1459
+ for memory_obj in memory_objs:
1460
+ if not memory_obj.is_valid():
1461
+ logger.warning("Trying to free an invalidated MemoryObj")
1462
+ continue
1463
+ memory_obj.invalidate()
1464
+
1465
+ if curr_start is None:
1466
+ curr_start = memory_obj.meta.address
1467
+ curr_size = memory_obj.meta.phy_size
1468
+ curr_count = 1
1469
+ elif curr_start + curr_size == memory_obj.meta.address:
1470
+ # Adjacent block, extend current
1471
+ curr_size += memory_obj.meta.phy_size
1472
+ curr_count += 1
1473
+ else:
1474
+ # Non-adjacent, save current and start new
1475
+ coalesced_blocks.append((curr_start, curr_size, curr_count))
1476
+ curr_start = memory_obj.meta.address
1477
+ curr_size = memory_obj.meta.phy_size
1478
+ curr_count = 1
1479
+
1480
+ if curr_start is not None:
1481
+ coalesced_blocks.append((curr_start, curr_size, curr_count))
1482
+
1483
+ # Free all coalesced blocks
1484
+ total_count = 0
1485
+ for address, size, count in coalesced_blocks:
1486
+ self.address_manager.free(address, size)
1487
+ total_count += count
1488
+
1489
+ # For debug
1490
+ self.num_active_allocations -= total_count
1491
+
1492
+ if update_stats:
1493
+ self.stats_monitor.update_local_cache_usage(
1494
+ self.address_manager.total_allocated_size
1495
+ )
1496
+ self.stats_monitor.update_active_memory_objs_count(
1497
+ self.num_active_allocations
1498
+ )
1499
+
1500
+ def memcheck(self):
1501
+ """For debug purposes.
1502
+ Returns True is everything is fine, otherwise False.
1503
+ """
1504
+ clear = True
1505
+ logger.info("Checking memory allocator consistency")
1506
+ logger.info(f" - Total active allocations: {self.num_active_allocations}")
1507
+ logger.info(
1508
+ f" - Total allocated size: "
1509
+ f"{self.address_manager.total_allocated_size / 1048576} MB"
1510
+ )
1511
+
1512
+ # Check the real total free size
1513
+ total_free_size = self.address_manager.get_free_size()
1514
+ logger.info(f" - Total free size: {total_free_size / 1048576} MB")
1515
+
1516
+ # Check if the numbers are consistent
1517
+ if (
1518
+ total_free_size + self.address_manager.total_allocated_size
1519
+ != self.address_manager.get_heap_size()
1520
+ ):
1521
+ logger.error("Memory allocator size is inconsistent")
1522
+ logger.error("This implies a bug in the memory allocator")
1523
+ clear = False
1524
+
1525
+ # Check if the blocks are coalesced
1526
+ if not self.address_manager.check_consistency():
1527
+ logger.error("Memory allocator has non-coalesced blocks")
1528
+ logger.error("This implies a bug in the memory allocator")
1529
+ clear = False
1530
+
1531
+ return clear
1532
+
1533
+ def __str__(self):
1534
+ return "TensorMemoryAllocator"
1535
+
1536
+
1537
+ class PagedAddressManager:
1538
+ """
1539
+ A lightweight address manager for PagedTensorMemoryAllocator.
1540
+ Provides get_free_size() and get_heap_size() by reading the
1541
+ paged allocator's state.
1542
+ """
1543
+
1544
+ def __init__(self, paged_allocator: "PagedTensorMemoryAllocator"):
1545
+ self._allocator = paged_allocator
1546
+
1547
+ def get_heap_size(self) -> int:
1548
+ """Get the total size of the paged address space in bytes."""
1549
+ return self._allocator.buffer_size
1550
+
1551
+ def get_free_size(self) -> int:
1552
+ """Get the total free size in bytes."""
1553
+ return len(self._allocator.free_blocks) * self._allocator.align_bytes
1554
+
1555
+
1556
+ class PagedTensorMemoryAllocator(MemoryAllocatorInterface):
1557
+ """
1558
+ Implements a paged memory allocator.
1559
+ """
1560
+
1561
+ def __init__(
1562
+ self,
1563
+ tensor: torch.Tensor,
1564
+ shapes: list[torch.Size],
1565
+ dtypes: list[torch.dtype],
1566
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1567
+ ):
1568
+ self.buffer = tensor.view(torch.uint8).flatten()
1569
+ self.buffer_size = self.buffer.numel() * self.buffer.element_size()
1570
+ self.buffer_ptr = self.buffer.data_ptr()
1571
+
1572
+ self.shapes = shapes
1573
+ self.dtypes = dtypes
1574
+ self.fmt = fmt
1575
+
1576
+ # full chunk size bytes
1577
+ self.align_bytes = get_size_bytes(shapes, dtypes)
1578
+
1579
+ assert self.buffer_size % self.align_bytes == 0, (
1580
+ f"Buffer size {self.buffer_size} must be a"
1581
+ f" multiple of align bytes {self.align_bytes}"
1582
+ " in paged memory allocator."
1583
+ )
1584
+
1585
+ self.paged_buffers = torch.split(self.buffer, self.align_bytes, dim=0)
1586
+
1587
+ # NOTE: deque is used since thread-safety is not a concern here as
1588
+ # is implemented in C under the hood (in CPython), and operations
1589
+ # on deque are atomic.
1590
+ self.free_blocks: deque[TensorMemoryObj] = deque()
1591
+
1592
+ for idx, buf in enumerate(self.paged_buffers):
1593
+ # NOTE: idx is the paged index
1594
+ # NOTE: the last unfull chunk's shape needs to be
1595
+ # adjusted during allocation.
1596
+ metadata = MemoryObjMetadata(
1597
+ self.shapes[0],
1598
+ self.dtypes[0],
1599
+ idx,
1600
+ self.align_bytes, # 1 page
1601
+ 1, # ref_count=1
1602
+ 0, # pin_count=0
1603
+ self.fmt,
1604
+ shapes=self.shapes,
1605
+ dtypes=self.dtypes,
1606
+ )
1607
+ mem_obj = TensorMemoryObj(
1608
+ raw_data=buf,
1609
+ metadata=metadata,
1610
+ parent_allocator=self,
1611
+ )
1612
+ self.free_blocks.append(mem_obj)
1613
+
1614
+ # Address manager for memory usage tracking
1615
+ self.address_manager = PagedAddressManager(self)
1616
+
1617
+ # For debugging purposes
1618
+ self.num_active_allocations = 0
1619
+ self.total_allocated_size = 0
1620
+
1621
+ self.stats_monitor = LMCStatsMonitor.GetOrCreate()
1622
+ logger.info(
1623
+ "Paged tensor memory allocator initialized, "
1624
+ "shapes: %s, dtypes: %s, align bytes: %s",
1625
+ self.shapes,
1626
+ self.dtypes,
1627
+ self.align_bytes,
1628
+ )
1629
+
1630
+ @_lmcache_nvtx_annotate
1631
+ def allocate(
1632
+ self,
1633
+ shapes: Union[torch.Size, list[torch.Size]],
1634
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1635
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1636
+ allocator_type: Optional[str] = None,
1637
+ ) -> Optional[TensorMemoryObj]:
1638
+ shapes, dtypes = self._adapt_shapes_and_dtypes(shapes, dtypes)
1639
+
1640
+ try:
1641
+ free_block = self.free_blocks.popleft()
1642
+ except IndexError:
1643
+ logger.debug(
1644
+ f"Failed to allocate memory for "
1645
+ f"tensor({shapes}, {dtypes}) because "
1646
+ "no free blocks is available"
1647
+ )
1648
+ return None
1649
+
1650
+ # TODO (Jiayi): This is a bit redundant.
1651
+ free_block.meta.shape = shapes[0]
1652
+ free_block.meta.dtype = dtypes[0]
1653
+ free_block.meta.shapes = shapes
1654
+ free_block.meta.dtypes = dtypes
1655
+ free_block.meta.fmt = fmt
1656
+ free_block.meta.ref_count = 1
1657
+
1658
+ if shapes != self.shapes:
1659
+ size_in_bytes = get_size_bytes(shapes, dtypes)
1660
+ free_block.raw_data = free_block.raw_data[:size_in_bytes]
1661
+
1662
+ # TODO (Jiayi): need a flag to drop these debug ops
1663
+ # NOTE (Jiayi): the following code is not thread-safe but
1664
+ # is tolerable as this is only used for debugging purposes.
1665
+ # Update debug status
1666
+ self.num_active_allocations += 1
1667
+ self.total_allocated_size += self.align_bytes
1668
+ self.stats_monitor.update_local_cache_usage(self.total_allocated_size)
1669
+ self.stats_monitor.update_active_memory_objs_count(self.num_active_allocations)
1670
+
1671
+ # Allocate the block
1672
+ return free_block
1673
+
1674
+ @_lmcache_nvtx_annotate
1675
+ def batched_allocate(
1676
+ self,
1677
+ shapes: Union[torch.Size, list[torch.Size]],
1678
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1679
+ batch_size: int,
1680
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1681
+ allocator_type: Optional[str] = None,
1682
+ ) -> Optional[List[TensorMemoryObj]]:
1683
+ """
1684
+ Batched allocate tensor memory objs with pre-defined equal sizes.
1685
+ """
1686
+ shapes, dtypes = self._adapt_shapes_and_dtypes(shapes, dtypes)
1687
+
1688
+ allocated_blocks: list[TensorMemoryObj] = []
1689
+ for i in range(batch_size):
1690
+ try:
1691
+ free_block = self.free_blocks.popleft()
1692
+ except IndexError:
1693
+ logger.debug(
1694
+ f"Failed to allocate memory for "
1695
+ f"tensor({shapes}, {dtypes}) because "
1696
+ "no free blocks is available"
1697
+ )
1698
+ self.batched_free(allocated_blocks, update_stats=False)
1699
+ return None
1700
+
1701
+ # FIXME: think about whether pareant_allocator
1702
+ # should be updated here.
1703
+ free_block.meta.shape = shapes[0]
1704
+ free_block.meta.dtype = dtypes[0]
1705
+ free_block.meta.shapes = shapes
1706
+ free_block.meta.dtypes = dtypes
1707
+ free_block.meta.fmt = fmt
1708
+ free_block.meta.ref_count = 1
1709
+
1710
+ if shapes != self.shapes:
1711
+ size_in_bytes = get_size_bytes(shapes, dtypes)
1712
+ free_block.raw_data = free_block.raw_data[:size_in_bytes]
1713
+
1714
+ allocated_blocks.append(free_block)
1715
+
1716
+ # TODO (Jiayi): need a flag to drop these debug ops
1717
+ # NOTE (Jiayi): the following code is not thread-safe but
1718
+ # is tolerable as this is only used for debugging purposes.
1719
+ # Update debug status
1720
+ self.num_active_allocations += batch_size
1721
+ self.total_allocated_size = self.num_active_allocations * self.align_bytes
1722
+ self.stats_monitor.update_local_cache_usage(self.total_allocated_size)
1723
+ self.stats_monitor.update_active_memory_objs_count(self.num_active_allocations)
1724
+
1725
+ # Allocate the block
1726
+ return allocated_blocks
1727
+
1728
+ @_lmcache_nvtx_annotate
1729
+ def free(self, memory_obj: TensorMemoryObj, allocator_type: Optional[str] = None):
1730
+ if not memory_obj.is_valid():
1731
+ return
1732
+ if memory_obj.meta.shapes != self.shapes:
1733
+ page_idx = memory_obj.meta.address
1734
+ memory_obj.raw_data = self.paged_buffers[page_idx]
1735
+
1736
+ self.free_blocks.append(memory_obj)
1737
+
1738
+ # memory_obj.invalidate()
1739
+
1740
+ # TODO (Jiayi): need a flag to drop these debug ops
1741
+ # NOTE (Jiayi): the following code is not thread-safe but
1742
+ # is tolerable as this is only used for debugging purposes.
1743
+ # Update debug status
1744
+ self.total_allocated_size -= self.align_bytes
1745
+ self.num_active_allocations -= 1
1746
+ self.stats_monitor.update_local_cache_usage(self.total_allocated_size)
1747
+ self.stats_monitor.update_active_memory_objs_count(self.num_active_allocations)
1748
+
1749
+ @_lmcache_nvtx_annotate
1750
+ def batched_free(
1751
+ self,
1752
+ memory_objs: List[TensorMemoryObj],
1753
+ allocator_type: Optional[str] = None,
1754
+ update_stats: bool = True,
1755
+ ):
1756
+ """
1757
+ Batched free memory objs.
1758
+ Unlike `batched_allocate`, this function does not
1759
+ assume that the memory objs are equal-sized.
1760
+ """
1761
+ if not memory_objs:
1762
+ return
1763
+
1764
+ for memory_obj in memory_objs:
1765
+ if not memory_obj.is_valid():
1766
+ logger.warning("Trying to free an invalidated MemoryObj")
1767
+ continue
1768
+ # memory_obj.invalidate()
1769
+ if memory_obj.meta.shapes != self.shapes:
1770
+ page_idx = memory_obj.meta.address
1771
+ memory_obj.raw_data = self.paged_buffers[page_idx]
1772
+
1773
+ self.free_blocks.append(memory_obj)
1774
+
1775
+ if update_stats:
1776
+ num_freed_blocks = len(memory_objs)
1777
+ # TODO (Jiayi): need a flag to drop these debug ops
1778
+ # NOTE (Jiayi): the following code is not thread-safe but
1779
+ # is tolerable as this is only used for debugging purposes.
1780
+ # Update debug status
1781
+ self.total_allocated_size -= self.align_bytes * num_freed_blocks
1782
+ self.num_active_allocations -= num_freed_blocks
1783
+ self.stats_monitor.update_local_cache_usage(self.total_allocated_size)
1784
+ self.stats_monitor.update_active_memory_objs_count(
1785
+ self.num_active_allocations
1786
+ )
1787
+
1788
+ def memcheck(self):
1789
+ """For debug purposes.
1790
+ Returns True is everything is fine, otherwise False.
1791
+ """
1792
+
1793
+ logger.info("Checking memory allocator consistency")
1794
+ logger.info(f" - Total active allocations: {self.num_active_allocations}")
1795
+ logger.info(
1796
+ f" - Total allocated size: {self.total_allocated_size / 1048576} MB"
1797
+ )
1798
+
1799
+ # Check the real total free size
1800
+ total_free_size = len(self.free_blocks) * self.align_bytes
1801
+ logger.info(f" - Total free size: {total_free_size / 1048576} MB")
1802
+
1803
+ # Check if the numbers are consistent
1804
+ if total_free_size + self.total_allocated_size != self.buffer.numel():
1805
+ logger.error("Memory allocator size is inconsistent")
1806
+ logger.error("This implies a bug in the memory allocator")
1807
+ return False
1808
+
1809
+ return True
1810
+
1811
+ def __str__(self):
1812
+ return "PagedTensorMemoryAllocator"
1813
+
1814
+ def __del__(self):
1815
+ # FIXME: NIXL-related memory leak should be handled somewhere (else).
1816
+ del self.buffer
1817
+
1818
+
1819
+ class BufferAllocator(MemoryAllocatorInterface):
1820
+ """Allocates memory in the pre-allocated pinned memory."""
1821
+
1822
+ def __init__(self, device="cpu"):
1823
+ """
1824
+ :param str device: The device of the buffer memory.
1825
+ """
1826
+ self.device = device
1827
+
1828
+ @_lmcache_nvtx_annotate
1829
+ def allocate(
1830
+ self,
1831
+ shapes: Union[torch.Size, list[torch.Size]],
1832
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1833
+ fmt: MemoryFormat = MemoryFormat.BINARY_BUFFER,
1834
+ allocator_type: Optional[str] = None,
1835
+ ) -> BytesBufferMemoryObj:
1836
+ if isinstance(shapes, list):
1837
+ n = shapes[0][0]
1838
+ else:
1839
+ n = shapes[0]
1840
+ byte_array = bytearray(n)
1841
+ return BytesBufferMemoryObj(byte_array)
1842
+
1843
+ @_lmcache_nvtx_annotate
1844
+ def batched_allocate(
1845
+ self,
1846
+ shapes: Union[torch.Size, list[torch.Size]],
1847
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1848
+ batch_size: int,
1849
+ fmt: MemoryFormat = MemoryFormat.BINARY_BUFFER,
1850
+ allocator_type: Optional[str] = None,
1851
+ ) -> List[BytesBufferMemoryObj]:
1852
+ if isinstance(shapes, list):
1853
+ n = shapes[0][0]
1854
+ else:
1855
+ n = shapes[0]
1856
+ # TODO(Jiayi): Optimize the following loop.
1857
+ byte_arrays = [bytearray(n) for _ in range(batch_size)]
1858
+ return [BytesBufferMemoryObj(byte_array) for byte_array in byte_arrays]
1859
+
1860
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
1861
+ return
1862
+
1863
+ def batched_free(
1864
+ self,
1865
+ memory_objs: List[MemoryObj],
1866
+ allocator_type: Optional[str] = None,
1867
+ update_stats: bool = True,
1868
+ ):
1869
+ return
1870
+
1871
+ def __str__(self):
1872
+ return "BufferAllocator"
1873
+
1874
+ def memcheck(self):
1875
+ return True
1876
+
1877
+
1878
+ class HostMemoryAllocator(MemoryAllocatorInterface):
1879
+ """Allocates memory in the pre-allocated Host memory."""
1880
+
1881
+ def __init__(self, size: int, use_paging: bool = False, **kwargs):
1882
+ """
1883
+ :param int size: The size of the pinned memory in bytes.
1884
+ """
1885
+ buffer = torch.empty(size, dtype=torch.uint8, device="cpu")
1886
+
1887
+ self.allocator: MemoryAllocatorInterface
1888
+ if use_paging:
1889
+ assert "shapes" in kwargs, (
1890
+ "shapes must be specified for paged memory allocator"
1891
+ )
1892
+ assert "dtypes" in kwargs, (
1893
+ "dtypes must be specified for paged memory allocator"
1894
+ )
1895
+ assert "fmt" in kwargs, "fmt must be specified for paged memory allocator"
1896
+ self.allocator = PagedTensorMemoryAllocator(
1897
+ tensor=buffer,
1898
+ shapes=kwargs["shapes"],
1899
+ dtypes=kwargs["dtypes"],
1900
+ fmt=kwargs["fmt"],
1901
+ )
1902
+ else:
1903
+ self.allocator = TensorMemoryAllocator(buffer)
1904
+
1905
+ self.host_mem_lock = threading.Lock() if not use_paging else nullcontext()
1906
+
1907
+ @_lmcache_nvtx_annotate
1908
+ def allocate(
1909
+ self,
1910
+ shapes: Union[torch.Size, list[torch.Size]],
1911
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1912
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1913
+ allocator_type: Optional[str] = None,
1914
+ ) -> Optional[MemoryObj]:
1915
+ with self.host_mem_lock:
1916
+ return self.allocator.allocate(shapes, dtypes, fmt, str(self))
1917
+
1918
+ @_lmcache_nvtx_annotate
1919
+ def batched_allocate(
1920
+ self,
1921
+ shapes: Union[torch.Size, list[torch.Size]],
1922
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1923
+ batch_size: int,
1924
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1925
+ allocator_type: Optional[str] = None,
1926
+ ) -> Optional[List[MemoryObj]]:
1927
+ with self.host_mem_lock:
1928
+ return self.allocator.batched_allocate(
1929
+ shapes, dtypes, batch_size, fmt, str(self)
1930
+ )
1931
+
1932
+ @_lmcache_nvtx_annotate
1933
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
1934
+ with self.host_mem_lock:
1935
+ self.allocator.free(memory_obj)
1936
+
1937
+ @_lmcache_nvtx_annotate
1938
+ def batched_free(
1939
+ self,
1940
+ memory_objs: List[MemoryObj],
1941
+ allocator_type: Optional[str] = None,
1942
+ update_stats: bool = True,
1943
+ ):
1944
+ with self.host_mem_lock:
1945
+ self.allocator.batched_free(memory_objs)
1946
+
1947
+ def memcheck(self):
1948
+ with self.host_mem_lock:
1949
+ return self.allocator.memcheck()
1950
+
1951
+ def __str__(self):
1952
+ return "HostMemoryAllocator"
1953
+
1954
+
1955
+ class PinMemoryAllocator(MemoryAllocatorInterface):
1956
+ """Allocates memory in the pre-allocated pinned memory."""
1957
+
1958
+ def __init__(self, size: int, use_paging: bool = False, **kwargs):
1959
+ """
1960
+ :param int size: The size of the pinned memory in bytes.
1961
+ """
1962
+ self.size = size
1963
+ self.buffer = _allocate_cpu_memory(size)
1964
+ self._unregistered = False
1965
+
1966
+ self.allocator: MemoryAllocatorInterface
1967
+ if use_paging:
1968
+ assert "shapes" in kwargs, (
1969
+ "shapes must be specified for paged memory allocator"
1970
+ )
1971
+ assert "dtypes" in kwargs, (
1972
+ "dtypes must be specified for paged memory allocator"
1973
+ )
1974
+ assert "fmt" in kwargs, "fmt must be specified for paged memory allocator"
1975
+ self.allocator = PagedTensorMemoryAllocator(
1976
+ tensor=self.buffer,
1977
+ shapes=kwargs["shapes"],
1978
+ dtypes=kwargs["dtypes"],
1979
+ fmt=kwargs["fmt"],
1980
+ )
1981
+ else:
1982
+ self.allocator = TensorMemoryAllocator(self.buffer)
1983
+
1984
+ self.host_mem_lock = threading.Lock() if not use_paging else nullcontext()
1985
+
1986
+ @_lmcache_nvtx_annotate
1987
+ def allocate(
1988
+ self,
1989
+ shapes: Union[torch.Size, list[torch.Size]],
1990
+ dtypes: Union[torch.dtype, list[torch.dtype]],
1991
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
1992
+ allocator_type: Optional[str] = None,
1993
+ ) -> Optional[MemoryObj]:
1994
+ with self.host_mem_lock:
1995
+ return self.allocator.allocate(shapes, dtypes, fmt, str(self))
1996
+
1997
+ @_lmcache_nvtx_annotate
1998
+ def batched_allocate(
1999
+ self,
2000
+ shapes: Union[torch.Size, list[torch.Size]],
2001
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2002
+ batch_size: int,
2003
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2004
+ allocator_type: Optional[str] = None,
2005
+ ) -> Optional[List[MemoryObj]]:
2006
+ with self.host_mem_lock:
2007
+ return self.allocator.batched_allocate(
2008
+ shapes, dtypes, batch_size, fmt, str(self)
2009
+ )
2010
+
2011
+ @_lmcache_nvtx_annotate
2012
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
2013
+ with self.host_mem_lock:
2014
+ self.allocator.free(memory_obj)
2015
+
2016
+ @_lmcache_nvtx_annotate
2017
+ def batched_free(
2018
+ self,
2019
+ memory_objs: List[MemoryObj],
2020
+ allocator_type: Optional[str] = None,
2021
+ update_stats: bool = True,
2022
+ ):
2023
+ with self.host_mem_lock:
2024
+ self.allocator.batched_free(memory_objs)
2025
+
2026
+ def memcheck(self):
2027
+ with self.host_mem_lock:
2028
+ return self.allocator.memcheck()
2029
+
2030
+ def close(self):
2031
+ if not self._unregistered:
2032
+ if self.buffer.numel() == 0:
2033
+ return
2034
+ _free_cpu_memory(self.buffer, self.size)
2035
+ self._unregistered = True
2036
+
2037
+ def __str__(self):
2038
+ return "PinMemoryAllocator"
2039
+
2040
+
2041
+ class MixedMemoryAllocator(MemoryAllocatorInterface):
2042
+ """
2043
+ Allocates (1) memory in the pre-allocated pinned memory.
2044
+ (2) byte_array buffer memory.
2045
+ """
2046
+
2047
+ def __init__(self, size: int, use_paging: bool = False, **kwargs):
2048
+ """
2049
+ :param int size: The size of the pinned memory in bytes.
2050
+ """
2051
+
2052
+ self.numa_mapping = kwargs.get("numa_mapping", None)
2053
+ self.align_bytes = kwargs.get("align_bytes", AddressManager.ALIGN_BYTES)
2054
+ if self.align_bytes <= 0 or self.align_bytes & (self.align_bytes - 1) != 0:
2055
+ raise ValueError("align_bytes must be a positive power of two")
2056
+
2057
+ # Extract shm_name from config.extra_config if available
2058
+ config = kwargs.get("config", None)
2059
+ if config is not None:
2060
+ self.shm_name: Optional[str] = config.get_extra_config_value(
2061
+ "shm_name", None
2062
+ )
2063
+ else:
2064
+ self.shm_name = kwargs.get("shm_name", None)
2065
+
2066
+ self.size = size
2067
+
2068
+ self.buffer = _allocate_cpu_memory(size, self.numa_mapping, self.shm_name)
2069
+
2070
+ self._unregistered = False
2071
+
2072
+ self.pin_allocator: MemoryAllocatorInterface
2073
+ if use_paging:
2074
+ assert "shapes" in kwargs, (
2075
+ "shapes must be specified for paged memory allocator"
2076
+ )
2077
+ assert "dtypes" in kwargs, (
2078
+ "dtypes must be specified for paged memory allocator"
2079
+ )
2080
+ assert "fmt" in kwargs, "fmt must be specified for paged memory allocator"
2081
+ self.pin_allocator = PagedTensorMemoryAllocator(
2082
+ tensor=self.buffer,
2083
+ shapes=kwargs["shapes"],
2084
+ dtypes=kwargs["dtypes"],
2085
+ fmt=kwargs["fmt"],
2086
+ )
2087
+ else:
2088
+ self.pin_allocator = TensorMemoryAllocator(
2089
+ self.buffer, align_bytes=self.align_bytes
2090
+ )
2091
+
2092
+ self.host_mem_lock = threading.Lock() if not use_paging else nullcontext()
2093
+
2094
+ self.buffer_allocator = BufferAllocator("cpu")
2095
+
2096
+ @_lmcache_nvtx_annotate
2097
+ def allocate(
2098
+ self,
2099
+ shapes: Union[torch.Size, list[torch.Size]],
2100
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2101
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2102
+ allocator_type: Optional[str] = None,
2103
+ ) -> Optional[MemoryObj]:
2104
+ if fmt == MemoryFormat.BINARY_BUFFER:
2105
+ return self.buffer_allocator.allocate(shapes, dtypes, fmt)
2106
+ elif fmt in [
2107
+ MemoryFormat.KV_2LTD,
2108
+ MemoryFormat.KV_2TD,
2109
+ MemoryFormat.KV_T2D,
2110
+ MemoryFormat.KV_MLA_FMT,
2111
+ ]:
2112
+ with self.host_mem_lock:
2113
+ return self.pin_allocator.allocate(shapes, dtypes, fmt, str(self))
2114
+ else:
2115
+ raise ValueError(f"Unsupported memory format: {fmt}")
2116
+
2117
+ @_lmcache_nvtx_annotate
2118
+ def batched_allocate(
2119
+ self,
2120
+ shapes: Union[torch.Size, list[torch.Size]],
2121
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2122
+ batch_size: int,
2123
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2124
+ allocator_type: Optional[str] = None,
2125
+ ) -> Optional[List[MemoryObj]]:
2126
+ if fmt == MemoryFormat.BINARY_BUFFER:
2127
+ return self.buffer_allocator.batched_allocate(
2128
+ shapes, dtypes, batch_size, fmt
2129
+ )
2130
+ elif fmt in [
2131
+ MemoryFormat.KV_2LTD,
2132
+ MemoryFormat.KV_2TD,
2133
+ MemoryFormat.KV_T2D,
2134
+ MemoryFormat.KV_MLA_FMT,
2135
+ ]:
2136
+ with self.host_mem_lock:
2137
+ return self.pin_allocator.batched_allocate(
2138
+ shapes, dtypes, batch_size, fmt, str(self)
2139
+ )
2140
+ else:
2141
+ raise ValueError(f"Unsupported memory format: {fmt}")
2142
+
2143
+ @_lmcache_nvtx_annotate
2144
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
2145
+ fmt = memory_obj.meta.fmt
2146
+ if fmt == MemoryFormat.BINARY_BUFFER:
2147
+ self.buffer_allocator.free(memory_obj)
2148
+ elif fmt in [
2149
+ MemoryFormat.KV_2LTD,
2150
+ MemoryFormat.KV_2TD,
2151
+ MemoryFormat.KV_T2D,
2152
+ MemoryFormat.KV_MLA_FMT,
2153
+ ]:
2154
+ with self.host_mem_lock:
2155
+ self.pin_allocator.free(memory_obj)
2156
+ else:
2157
+ raise ValueError(f"Unsupported memory format: {fmt}")
2158
+
2159
+ @_lmcache_nvtx_annotate
2160
+ def batched_free(
2161
+ self,
2162
+ memory_objs: List[MemoryObj],
2163
+ allocator_type: Optional[str] = None,
2164
+ update_stats: bool = True,
2165
+ ):
2166
+ if not memory_objs:
2167
+ return
2168
+
2169
+ # NOTE: fmts of all memory_objs should be the same
2170
+ fmt = memory_objs[0].meta.fmt
2171
+ if fmt == MemoryFormat.BINARY_BUFFER:
2172
+ self.buffer_allocator.batched_free(memory_objs)
2173
+ elif fmt in [
2174
+ MemoryFormat.KV_2LTD,
2175
+ MemoryFormat.KV_2TD,
2176
+ MemoryFormat.KV_T2D,
2177
+ MemoryFormat.KV_MLA_FMT,
2178
+ ]:
2179
+ with self.host_mem_lock:
2180
+ self.pin_allocator.batched_free(memory_objs)
2181
+ else:
2182
+ raise ValueError(f"Unsupported memory format: {fmt}")
2183
+
2184
+ def memcheck(self):
2185
+ with self.host_mem_lock:
2186
+ return self.pin_allocator.memcheck()
2187
+
2188
+ def close(self):
2189
+ if not self._unregistered:
2190
+ if torch.cuda.is_available():
2191
+ torch.cuda.synchronize()
2192
+ if self.buffer.numel() == 0:
2193
+ return
2194
+ _free_cpu_memory(
2195
+ self.buffer,
2196
+ self.size,
2197
+ self.numa_mapping,
2198
+ self.shm_name,
2199
+ )
2200
+ self._unregistered = True
2201
+
2202
+ def __str__(self):
2203
+ return "MixedMemoryAllocator"
2204
+
2205
+
2206
+ class GPUMemoryAllocator(MemoryAllocatorInterface):
2207
+ """Allocates memory in the pre-allocated GPU memory."""
2208
+
2209
+ def __init__(
2210
+ self,
2211
+ size: int,
2212
+ device="cuda",
2213
+ align_bytes: Optional[int] = None,
2214
+ use_paging: bool = False,
2215
+ **kwargs,
2216
+ ):
2217
+ """
2218
+ :param int size: The size of the GPU memory in bytes.
2219
+ :param Optional[int] align_bytes: The byte alignment for allocations.
2220
+ """
2221
+ if not torch.cuda.is_available():
2222
+ device = "cpu"
2223
+
2224
+ self.tensor = torch.empty(size, dtype=torch.uint8, device=device)
2225
+
2226
+ self.allocator: MemoryAllocatorInterface
2227
+ if use_paging:
2228
+ assert "shapes" in kwargs, (
2229
+ "shapes must be specified for paged memory allocator"
2230
+ )
2231
+ assert "dtypes" in kwargs, (
2232
+ "dtypes must be specified for paged memory allocator"
2233
+ )
2234
+ assert "fmt" in kwargs, "fmt must be specified for paged memory allocator"
2235
+ self.allocator = PagedTensorMemoryAllocator(
2236
+ tensor=self.tensor,
2237
+ shapes=kwargs["shapes"],
2238
+ dtypes=kwargs["dtypes"],
2239
+ fmt=kwargs["fmt"],
2240
+ )
2241
+ else:
2242
+ kwargs = {}
2243
+ if align_bytes is not None:
2244
+ kwargs["align_bytes"] = align_bytes
2245
+ self.allocator = TensorMemoryAllocator(self.tensor, **kwargs)
2246
+
2247
+ self.device_mem_lock = threading.Lock() if not use_paging else nullcontext()
2248
+
2249
+ @_lmcache_nvtx_annotate
2250
+ def allocate(
2251
+ self,
2252
+ shapes: Union[torch.Size, list[torch.Size]],
2253
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2254
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2255
+ allocator_type: Optional[str] = None,
2256
+ ) -> Optional[MemoryObj]:
2257
+ with self.device_mem_lock:
2258
+ return self.allocator.allocate(shapes, dtypes, fmt, str(self))
2259
+
2260
+ @_lmcache_nvtx_annotate
2261
+ def batched_allocate(
2262
+ self,
2263
+ shapes: Union[torch.Size, list[torch.Size]],
2264
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2265
+ batch_size: int,
2266
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2267
+ allocator_type: Optional[str] = None,
2268
+ ) -> Optional[List[MemoryObj]]:
2269
+ with self.device_mem_lock:
2270
+ return self.allocator.batched_allocate(
2271
+ shapes, dtypes, batch_size, fmt, str(self)
2272
+ )
2273
+
2274
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
2275
+ with self.device_mem_lock:
2276
+ self.allocator.free(memory_obj)
2277
+
2278
+ def batched_free(
2279
+ self,
2280
+ memory_objs: List[MemoryObj],
2281
+ allocator_type: Optional[str] = None,
2282
+ update_stats: bool = True,
2283
+ ):
2284
+ with self.device_mem_lock:
2285
+ self.allocator.batched_free(memory_objs)
2286
+
2287
+ def memcheck(self):
2288
+ with self.device_mem_lock:
2289
+ return self.allocator.memcheck()
2290
+
2291
+ def __str__(self):
2292
+ return "GPUMemoryAllocator"
2293
+
2294
+
2295
+ class AdHocMemoryAllocator(MemoryAllocatorInterface):
2296
+ """
2297
+ AdHocMemoryAllocator is a simple allocator that does not actually
2298
+ allocate memory. It is used for testing purposes only.
2299
+ """
2300
+
2301
+ def __init__(self, device: str = "cpu"):
2302
+ """
2303
+ :param str device: The device of the ad hoc memory allocator.
2304
+ """
2305
+ if not torch.cuda.is_available():
2306
+ self.device = "cpu"
2307
+ else:
2308
+ self.device = device
2309
+
2310
+ @_lmcache_nvtx_annotate
2311
+ def allocate(
2312
+ self,
2313
+ shapes: Union[torch.Size, list[torch.Size]],
2314
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2315
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2316
+ allocator_type: Optional[str] = None,
2317
+ ) -> Optional[MemoryObj]:
2318
+ """
2319
+ Returns a dummy MemoryObj for testing purposes.
2320
+ """
2321
+ shapes, dtypes = self._adapt_shapes_and_dtypes(shapes, dtypes)
2322
+ size = get_size_bytes(shapes, dtypes)
2323
+
2324
+ # Return a dummy object with no actual memory allocation
2325
+ return TensorMemoryObj(
2326
+ raw_data=torch.empty(
2327
+ torch.Size([size]), dtype=torch.uint8, device=self.device
2328
+ ),
2329
+ metadata=MemoryObjMetadata(
2330
+ shape=shapes[0],
2331
+ dtype=dtypes[0],
2332
+ address=0,
2333
+ phy_size=0,
2334
+ ref_count=1,
2335
+ pin_count=0,
2336
+ fmt=fmt,
2337
+ shapes=shapes,
2338
+ dtypes=dtypes,
2339
+ ),
2340
+ parent_allocator=self,
2341
+ )
2342
+
2343
+ @_lmcache_nvtx_annotate
2344
+ def batched_allocate(
2345
+ self,
2346
+ shapes: Union[torch.Size, list[torch.Size]],
2347
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2348
+ batch_size: int,
2349
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2350
+ allocator_type: Optional[str] = None,
2351
+ ) -> Optional[List[MemoryObj]]:
2352
+ raise NotImplementedError(
2353
+ "Batched allocation is not supported in AdHocMemoryAllocator"
2354
+ )
2355
+
2356
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
2357
+ pass
2358
+
2359
+ def batched_free(
2360
+ self,
2361
+ memory_objs: List[MemoryObj],
2362
+ allocator_type: Optional[str] = None,
2363
+ update_stats: bool = True,
2364
+ ):
2365
+ pass
2366
+
2367
+ def ref_count_up(self, memory_obj: MemoryObj):
2368
+ pass
2369
+
2370
+ def ref_count_down(self, memory_obj: MemoryObj):
2371
+ pass
2372
+
2373
+ def get_ref_count(self, memory_obj: MemoryObj):
2374
+ return 0
2375
+
2376
+ def memcheck(self):
2377
+ return True
2378
+
2379
+ def __str__(self):
2380
+ return "AdHocMemoryAllocator"
2381
+
2382
+
2383
+ class CuFileMemoryAllocator(GPUMemoryAllocator):
2384
+ def __init__(self, size: int, device=None):
2385
+ # HACK(Jiayi): cufile import is buggy on some hardware
2386
+ # (e.g., without GPUDirect), so it's temporarily put here.
2387
+ # Third Party
2388
+ from cufile.bindings import cuFileBufDeregister, cuFileBufRegister
2389
+
2390
+ self.cuFileBufDeregister = cuFileBufDeregister
2391
+ if device is None:
2392
+ # TODO(Serapheim): Ideally we'd get the device from the upper
2393
+ # layer - for now just use the current device.
2394
+ if torch.cuda.is_available():
2395
+ device = f"cuda:{torch.cuda.current_device()}"
2396
+ else:
2397
+ device = "cpu:0"
2398
+ super().__init__(size, device, align_bytes=4096)
2399
+ self.base_pointer = self.tensor.data_ptr()
2400
+ cuFileBufRegister(ctypes.c_void_p(self.base_pointer), size, flags=0)
2401
+
2402
+ def __del__(self):
2403
+ self.cuFileBufDeregister(ctypes.c_void_p(self.base_pointer))
2404
+
2405
+ def __str__(self):
2406
+ return "CuFileMemoryAllocator"
2407
+
2408
+
2409
+ class HipFileMemoryAllocator(GPUMemoryAllocator):
2410
+ def __init__(self, size: int, device=None):
2411
+ # HACK: hipfile import is placed here to avoid import errors on
2412
+ # hardware without GPUDirect Storage / hipFile support.
2413
+ # Third Party
2414
+ from hipfile.bindings import hipFileBufDeregister, hipFileBufRegister
2415
+
2416
+ self.hipFileBufDeregister = hipFileBufDeregister
2417
+ if device is None:
2418
+ if torch.cuda.is_available():
2419
+ # TODO: On ROCm, PyTorch still uses the CUDA API internally
2420
+ device = f"cuda:{torch.cuda.current_device()}"
2421
+ else:
2422
+ device = "cpu:0"
2423
+
2424
+ super().__init__(size, device, align_bytes=4096)
2425
+ self.base_pointer = self.tensor.data_ptr()
2426
+ hipFileBufRegister(ctypes.c_void_p(self.base_pointer), size, flags=0)
2427
+
2428
+ def __del__(self):
2429
+ self.hipFileBufDeregister(ctypes.c_void_p(self.base_pointer))
2430
+
2431
+ def __str__(self):
2432
+ return "HipFileMemoryAllocator"
2433
+
2434
+
2435
+ class PagedCpuGpuMemoryAllocator(MemoryAllocatorInterface):
2436
+ """
2437
+ Paged Memory Allocator for both CPU and GPU memory.
2438
+ This is a paged memory allocator for PD and P2P sharing
2439
+ when NIXL is enabled as NIXL relies on the paging abstraction.
2440
+ """
2441
+
2442
+ def __init__(self):
2443
+ pass
2444
+
2445
+ def init_gpu_memory_allocator(
2446
+ self,
2447
+ size: int,
2448
+ shapes: list[torch.Size],
2449
+ dtypes: list[torch.dtype],
2450
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2451
+ device: str = "cuda",
2452
+ ):
2453
+ self.gpu_buffer = torch.empty(
2454
+ size,
2455
+ dtype=torch.uint8,
2456
+ device=device,
2457
+ )
2458
+ self.gpu_allocator = PagedTensorMemoryAllocator(
2459
+ self.gpu_buffer,
2460
+ shapes,
2461
+ dtypes,
2462
+ fmt,
2463
+ )
2464
+
2465
+ def init_cpu_memory_allocator(
2466
+ self,
2467
+ size: int,
2468
+ shapes: list[torch.Size],
2469
+ dtypes: list[torch.dtype],
2470
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2471
+ numa_mapping: Optional[NUMAMapping] = None,
2472
+ ):
2473
+ self.cpu_buffer = _allocate_cpu_memory(size, numa_mapping)
2474
+ self.cpu_allocator = PagedTensorMemoryAllocator(
2475
+ self.cpu_buffer,
2476
+ shapes,
2477
+ dtypes,
2478
+ fmt,
2479
+ )
2480
+ self.align_bytes = self.cpu_allocator.align_bytes
2481
+
2482
+ def allocate(
2483
+ self,
2484
+ shapes: Union[torch.Size, list[torch.Size]],
2485
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2486
+ fmt: MemoryFormat = MemoryFormat.UNDEFINED,
2487
+ allocator_type: Optional[str] = "cpu",
2488
+ ) -> Optional[MemoryObj]:
2489
+ if allocator_type == "gpu":
2490
+ return self.gpu_allocator.allocate(shapes, dtypes, fmt)
2491
+ elif allocator_type == "cpu":
2492
+ return self.cpu_allocator.allocate(shapes, dtypes, fmt)
2493
+ else:
2494
+ raise ValueError(f"Unsupported allocator type: {allocator_type}")
2495
+
2496
+ def batched_allocate(
2497
+ self,
2498
+ shapes: Union[torch.Size, list[torch.Size]],
2499
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2500
+ batch_size: int,
2501
+ fmt: MemoryFormat = MemoryFormat.UNDEFINED,
2502
+ allocator_type: Optional[str] = "gpu",
2503
+ ) -> Optional[List[MemoryObj]]:
2504
+ if allocator_type == "gpu":
2505
+ return self.gpu_allocator.batched_allocate(shapes, dtypes, batch_size, fmt)
2506
+ elif allocator_type == "cpu":
2507
+ return self.cpu_allocator.batched_allocate(shapes, dtypes, batch_size, fmt)
2508
+ else:
2509
+ raise ValueError(f"Unsupported allocator type: {allocator_type}")
2510
+
2511
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = "cpu"):
2512
+ if allocator_type == "gpu":
2513
+ self.gpu_allocator.free(memory_obj)
2514
+ elif allocator_type == "cpu":
2515
+ self.cpu_allocator.free(memory_obj)
2516
+ else:
2517
+ raise ValueError(f"Unsupported allocator type: {allocator_type}")
2518
+
2519
+ def batched_free(
2520
+ self,
2521
+ memory_objs: List[MemoryObj],
2522
+ allocator_type: Optional[str] = None,
2523
+ update_stats: bool = True,
2524
+ ):
2525
+ if allocator_type == "gpu":
2526
+ self.gpu_allocator.batched_free(memory_objs, update_stats=update_stats)
2527
+ elif allocator_type == "cpu":
2528
+ self.cpu_allocator.batched_free(memory_objs, update_stats=update_stats)
2529
+ else:
2530
+ raise ValueError(f"Unsupported allocator type: {allocator_type}")
2531
+
2532
+ def __str__(self):
2533
+ return "PDMemoryAllocator"
2534
+
2535
+
2536
+ class XPUMemoryAllocator(MemoryAllocatorInterface):
2537
+ """Allocates memory in the pre-allocated XPU memory."""
2538
+
2539
+ def __init__(
2540
+ self,
2541
+ size: int,
2542
+ device="xpu",
2543
+ align_bytes: Optional[int] = None,
2544
+ use_paging: bool = False,
2545
+ **kwargs,
2546
+ ):
2547
+ self.tensor = torch.empty((size,), dtype=torch.uint8, device=device)
2548
+
2549
+ self.allocator: MemoryAllocatorInterface
2550
+ if use_paging:
2551
+ assert "shapes" in kwargs, (
2552
+ "shapes must be specified for paged memory allocator"
2553
+ )
2554
+ assert "dtypes" in kwargs, (
2555
+ "dtypes must be specified for paged memory allocator"
2556
+ )
2557
+ assert "fmt" in kwargs, "fmt must be specified for paged memory allocator"
2558
+ self.allocator = PagedTensorMemoryAllocator(
2559
+ tensor=self.tensor,
2560
+ shapes=kwargs["shapes"],
2561
+ dtypes=kwargs["dtypes"],
2562
+ fmt=kwargs["fmt"],
2563
+ )
2564
+ else:
2565
+ alloc_kwargs = {}
2566
+ if align_bytes is not None:
2567
+ alloc_kwargs["align_bytes"] = align_bytes
2568
+ self.allocator = TensorMemoryAllocator(self.tensor, **alloc_kwargs)
2569
+
2570
+ self.device_mem_lock = threading.Lock() if not use_paging else nullcontext()
2571
+
2572
+ @_lmcache_nvtx_annotate
2573
+ def allocate(
2574
+ self,
2575
+ shapes: Union[torch.Size, list[torch.Size]],
2576
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2577
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2578
+ allocator_type: Optional[str] = None,
2579
+ ) -> Optional[MemoryObj]:
2580
+ with self.device_mem_lock:
2581
+ return self.allocator.allocate(shapes, dtypes, fmt, str(self))
2582
+
2583
+ @_lmcache_nvtx_annotate
2584
+ def batched_allocate(
2585
+ self,
2586
+ shapes: Union[torch.Size, list[torch.Size]],
2587
+ dtypes: Union[torch.dtype, list[torch.dtype]],
2588
+ batch_size: int,
2589
+ fmt: MemoryFormat = MemoryFormat.KV_2LTD,
2590
+ allocator_type: Optional[str] = None,
2591
+ ) -> Optional[List[MemoryObj]]:
2592
+ with self.device_mem_lock:
2593
+ return self.allocator.batched_allocate(
2594
+ shapes, dtypes, batch_size, fmt, str(self)
2595
+ )
2596
+
2597
+ def free(self, memory_obj: MemoryObj, allocator_type: Optional[str] = None):
2598
+ with self.device_mem_lock:
2599
+ self.allocator.free(memory_obj)
2600
+
2601
+ def batched_free(
2602
+ self,
2603
+ memory_objs: List[MemoryObj],
2604
+ allocator_type: Optional[str] = None,
2605
+ update_stats: bool = True,
2606
+ ):
2607
+ with self.device_mem_lock:
2608
+ self.allocator.batched_free(memory_objs)
2609
+
2610
+ def memcheck(self):
2611
+ with self.device_mem_lock:
2612
+ return self.allocator.memcheck()
2613
+
2614
+ def close(self):
2615
+ if hasattr(torch, "xpu") and torch.xpu.is_available():
2616
+ torch.xpu.synchronize()
2617
+
2618
+ def __str__(self):
2619
+ return "XPUMemoryAllocator"